diff --git a/.gitignore b/.gitignore index ed17e4e..e2e07ac 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ .\#* *.orig *.rej +*.dat # matlab-load.el is generated matlab-load.el +parse \ No newline at end of file diff --git a/Makefile b/Makefile index 45604a3..c235167 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ LOADPATH= ./ LOADDEFS=matlab-load.el LOADDIRS=. misc_MISC=ChangeLog ChangeLog.old1 ChangeLog.old2 INSTALL README dl_emacs_support.m -lisp_LISP=matlab-compat.el matlab.el matlab-shell.el matlab-shell-gud.el matlab-netshell.el matlab-complete.el matlab-cgen.el matlab-publish.el matlab-topic.el mlint.el tlc.el linemark.el matlab-maint.el +lisp_LISP=matlab-compat.el matlab-syntax.el matlab-scan.el matlab.el matlab-shell.el matlab-shell-gud.el matlab-netshell.el matlab-complete.el matlab-cgen.el matlab-publish.el matlab-topic.el mlint.el tlc.el linemark.el matlab-maint.el cedet_LISP=semantic-matlab.el semanticdb-matlab.el srecode-matlab.el cedet-matlab.el company-matlab-shell.el VERSION=4.0 DISTDIR=$(top)matlab-emacs-$(VERSION) diff --git a/NEWS.org b/NEWS.org index 19025cc..1824484 100644 --- a/NEWS.org +++ b/NEWS.org @@ -1,5 +1,141 @@ * Changes and New Features in matlab-emacs +** New in 5.0 + +*** Syntax tables / Strings and Comments / Font lock + + Command and String syntax handling is now managed using syntax-table customization + + This results in: + * More flavors of syntax highlighting around commands and strings, including all of: + * strings, unterminated strings, commanddual strings + * comments, cellbreak comments, pragma comments, ignored comments, ellipssis + * Accurate differentiation between 'char arrays' and "strings" and quoted charts. + * Performance improvements for comment/string parsing. + + There is a new shorter 'ignore' comment type that starts with: %^ + + In addition, font lock of keywords is now more robust, with keywords not being + highlighted when they are not being used in the correct scope. + +*** Syntactic block navigation + + With proper syntax table support now available, built-in emacs commands that depend on + sexp now work, such as: + * up-list + * forward-sexp + * kill-sexp + * mark-sexp + + In addition, commands that work with defuns now all work correctly, such as: + * mark-defun + * narrow-to-defun + + All custom commands that used to implement these syntax behaviors have been removed, or + had their bindings removed, including: + * matlab-beginning-of-command + * matlab-end-of-command + * matlab-forward-sexp + * matlab-backward-sexp + * matlab-indent-sexp + * matlab-beginning-of-defun + * matlab-end-of-defn + + In addition syntactic block navigation is faster, where very large files can now be navigated + in fractions of a second that used to take a few minutes. + +*** Support for block validation + + Block navigation now does validation, for example, 'property' keywords should only occur + inside a classdef, and 'arguments' keywords should only occur inside a function. + + This means that you can now have variables and functions named 'property' and + 'arguments' of those words occur outside valid locations. + +*** Indentation + + Indentation performance is greatly improved. Based on our tests, large files that used + to take 10 minutes to indent will now complete in just 1 or 2 seconds. + + Several new indentation features exist, such as: + * correct indentation of arguemnts blocks + * improved indentation of function argument lists that span multiple lines. + * improved indentation around block comments + * improved indentation accuracy in classdef, property, method blocks. + * more accurate indentation of continuations + + Some indentation features were removed, such as: + * Max indent distance support inside function call args + * Max indent distance support inside switch statements + * Line-up rules inside ( ), [ ], and { } have changed subtly dependeing on + context after the opening (, [, or {. + + Specialty indentation commands have been removed: + * matlab-indent-sexp + + Electric indentation has been added to block keywords such as end, else, case, etc. + + Lots of bug fixes and general improvements for handling edge cases. + +*** matlab-return & friends removed + + The 'matlab-return' and related functions have all been removed. Features of these + commands are now part of Emacs' built in handling for RETURN and no longer need to be + part of matlab mode. + +*** File type detection + + File type detection has been improved. Previously matlab mode detected if functions had + ends, and if functions were indented. It now detects more cases, and displays results + in the status line. + + The list of detectable features are: + * function (with no end) + * function .. end + * classdef .. end + * scripts + * empty files + + Functions with ends also detect if function bodies are indented. Other kinds of + functions will always indent. + + The check for the type of file is also auto-re-detected on save, so if you change the + type of file while editing, it will automatically adjust. + +*** Auto verify changes + + Auto verify on save has been updated. + + 1. verify classname added - this will fix class names for you + 2. verify add ends - this now asks questions less often, or not at all. + In addition, it has a more robust algorithm for adding ends. + +*** mlint support questions + + mlint mode now supports many more auto fix behaviors, including: + * missing ends - with nicer guess for where the end goes. + * function name - auto fix function, class, and method names. + + Plus several minor bug fixes. + +*** Support for older Emacsen + + Support for Emacs older than Emacs 24 has been dropped. Many of the special + compatability layers needed to change, and the new code has not been tested against + older versions of Emacs. As a result, many compatability layers were removed. + +*** Test suite improvements + + The test suite that is part of matlab-emacs project has many more test points, and has + added support for testing font lock, performance, and other features. + +*** matlab-emacs maintenance mode + +There is now a support file 'matlab-maint' that simplifies the task of building and +testing matlab-mode during development. Consider using this library if you intend to +develop matlab-mode. + + ** News in 4.0 *** Debugging @@ -56,7 +192,7 @@ You have the following capabilities when in a MATLAB M-file: more responsive. For example, in matlab-shell, Emacs is more responsive when processing long output lines from MATLAB. -*** Bug fixex +*** Bug fixes - There are a number of bug fixes. *** Quotes diff --git a/Project.ede b/Project.ede index 688ae06..3a7541c 100644 --- a/Project.ede +++ b/Project.ede @@ -17,7 +17,7 @@ (ede-proj-target-elisp "ede-proj-target-elisp" :name "lisp" :path "" - :source '("matlab-compat.el" "matlab.el" "matlab-shell.el" "matlab-shell-gud.el" "matlab-netshell.el" "matlab-complete.el" "matlab-cgen.el" "matlab-publish.el" "matlab-topic.el" "mlint.el" "tlc.el" "linemark.el" "matlab-maint.el") + :source '("matlab-compat.el" "matlab-syntax.el" "matlab-scan.el" "matlab.el" "matlab-shell.el" "matlab-shell-gud.el" "matlab-netshell.el" "matlab-complete.el" "matlab-cgen.el" "matlab-publish.el" "matlab-topic.el" "mlint.el" "tlc.el" "linemark.el" "matlab-maint.el") :versionsource '("matlab.el") :aux-packages '("matlab")) (ede-proj-target-elisp "ede-proj-target-elisp" diff --git a/linemark.el b/linemark.el index 1115bb1..2711a94 100644 --- a/linemark.el +++ b/linemark.el @@ -275,7 +275,12 @@ Call the new entrie's activate method." (oset new-entry face (or face (oref g face))) (oset g marks (cons new-entry (oref g marks))) (if (oref g active) - (linemark-display new-entry t)) + (condition-case nil + ;; Somewhere in the eieio framework this can throw 'end of buffer' error + ;; after the display function exits. Not sure where that is, but this + ;; condition-case can capture it and allow things to keep going. + (linemark-display new-entry t) + (error nil))) new-entry) )) @@ -356,7 +361,12 @@ Call the new entrie's activate method." (defun linemark-find-file-hook () "Activate all marks which can benifit from this new buffer." - (mapcar (lambda (g) (linemark-display g t)) linemark-groups)) + (mapcar (lambda (g) (condition-case nil + ;; See comment in linemark-add-entry for + ;; reasoning on this condition-case. + (linemark-display g t) + (error nil))) + linemark-groups)) (defun linemark-kill-buffer-hook () "Deactivate all entries in the current buffer." diff --git a/matlab-cgen.el b/matlab-cgen.el index b96b1bb..37306e5 100644 --- a/matlab-cgen.el +++ b/matlab-cgen.el @@ -77,7 +77,9 @@ "Insert and END block based on the current syntax. Optional argument REINDENT indicates if the specified block should be re-indented." (interactive "P") - (if (not (matlab-ltype-empty)) (progn (end-of-line) (insert "\n"))) + (when (not (matlab-line-empty-p (matlab-compute-line-context 1))) + (end-of-line) (insert "\n")) + (let ((valid t) (begin nil)) (save-excursion (condition-case nil @@ -172,7 +174,8 @@ Optional argument REINDENT indicates if the specified block should be re-indente (error (setq valid nil)))) (if (not valid) (error "Not in a switch statement"))) - (if (not (matlab-ltype-empty)) (progn (end-of-line) (insert "\n"))) + (when (not (matlab-line-empty-p (matlab-compute-line-context 1))) + (end-of-line) (insert "\n")) (indent-to 0) (insert "case ") (matlab-indent-line)) @@ -234,14 +237,16 @@ the region. BEGIN and END mark the region to be stringified." "Spell check valid strings in region with Ispell. Argument BEGIN and END mark the region boundary." (interactive "r") + (error "This function needs to be reimplemented.") (require 'ispell) (save-excursion (goto-char begin) ;; Here we use the font lock function for finding strings. ;; Its cheap, fast, and accurate. ;; NOTE: This now also does comments - (while (and (matlab-font-lock-allstring-comment-match-normal end) - (ispell-region (match-beginning 0) (match-end 0)))))) + ;;(while (and (matlab-font-lock-allstring-comment-match-normal end) + ;; (ispell-region (match-beginning 0) (match-end 0)))) + )) (defun matlab-ispell-strings-and-comments () "Spell check valid strings in the current buffer with Ispell. diff --git a/matlab-compat.el b/matlab-compat.el index 05f757c..c035323 100644 --- a/matlab-compat.el +++ b/matlab-compat.el @@ -150,6 +150,11 @@ REGEXP defaults to \"[ \\t\\n\\r]+\"." out)) ) +;; OBARRAYS +(if (fboundp 'obarray-make) + (defalias 'matlab-obarray-make 'obarray-make) + (defun matlab-obarray-make (sz) (make-vector sz 0))) + ;; Finding executables (defun matlab-find-executable-directory (program) "Find the executable PROGRAM on the exec path, following any links. diff --git a/matlab-complete.el b/matlab-complete.el index 97cfd22..0874f9b 100644 --- a/matlab-complete.el +++ b/matlab-complete.el @@ -30,7 +30,21 @@ ;; and this can be loaded optionally. ;;; Code: +(require 'cl-macs) (require 'matlab) +(require 'matlab-shell) + +(defun matlab-uniquify-list (lst) + "Return a list that is a subset of LST where all elements are unique." + (if (fboundp 'cl-remove-duplicates) + (cl-remove-duplicates lst :test 'string= :from-end t) + ;; Else, do it by hand. + (let ((nlst nil)) + (while lst + (if (and (car lst) (not (member (car lst) nlst))) + (setq nlst (cons (car lst) nlst))) + (setq lst (cdr lst))) + (nreverse nlst)))) ;;; Customizations =========================================================== ;; @@ -181,9 +195,32 @@ This list still needs lots of help.") tl (cdr tl))) r)) +(defun matlab-lattr-semantics (&optional prefix) + "Return the semantics of the current position. +Values are nil 'solo, 'value, and 'boolean. Boolean is a subset of +value. nil means there is no semantic content (ie, string or comment.) +If optional PREFIX, then return 'solo if that is the only thing on the +line." + (cond + ((or (matlab-line-empty-p (matlab-compute-line-context 1)) + (and prefix (save-excursion + (beginning-of-line) + (looking-at (concat "\\s-*" prefix "\\s-*$"))))) + 'solo) + ((save-excursion + (matlab-beginning-of-command) + (looking-at "\\s-*\\(if\\|elseif\\|while\\)\\>")) + 'boolean) + ((save-excursion + (matlab-beginning-of-command) + (looking-at (concat "\\s-*\\(" (matlab-property-function) + "\\)\\>"))) + 'property) + (t + 'value))) + ;;; Completion Framework =================================================== ;; - (defun matlab-find-recent-variable-list (prefix) "Return a list of most recent variables starting with PREFIX as a string. Reverse searches for the following are done first: @@ -211,28 +248,28 @@ If the list is empty, then searches continue backwards through the code." (nreverse lst))) (save-excursion (let ((lst nil)) - (while (and (re-search-backward - (concat "\\<\\(" matlab-block-beg-pre-no-if - "\\)\\s-+(?\\s-*\\(" prefix - "\\w+\\)\\>") - bounds t) - (< (length lst) 10)) - (setq lst (cons (match-string 2) lst))) + (while (and + (< (length lst) 10) + (matlab-re-search-keyword-backward (matlab-keyword-regex 'ctrl) bounds t)) + (when (looking-at (concat "\\w+\\s-+(?\\(" prefix "\\w+\\)\\_>")) + (setq lst (cons (match-string 1) lst)))) (nreverse lst))) (save-excursion - (if (re-search-backward "^\\s-*global\\s-+" bounds t) - (let ((lst nil) m e) + (let ((lst nil) m e) + (while (matlab-re-search-keyword-backward + (matlab-keyword-regex 'vardecl) bounds t) + (save-excursion (goto-char (match-end 0)) - (while (looking-at "\\(\\w+\\)\\([ \t]+\\|$\\)") + (while (looking-at "\\s-*\\(\\w+\\)\\([ \t]+\\|$\\)") (setq m (match-string 1) e (match-end 0)) (if (equal 0 (string-match prefix m)) (setq lst (cons m lst))) - (goto-char e)) - (nreverse lst)))) + (goto-char e)))) + (nreverse lst))) (save-excursion (if (and (re-search-backward "^\\s-*function\\>" bounds t) - (re-search-forward "\\<\\(\\w+\\)(" + (re-search-forward "\\_<\\(\\w+\\)\\s-*(" (matlab-point-at-eol) t)) (let ((lst nil) m e) (while (looking-at "\\(\\w+\\)\\s-*[,)]\\s-*") @@ -278,7 +315,7 @@ In NEXT is non-nil, than continue through the list of elements." (let ((lst nil)) (while (re-search-forward "^\\s-*function\\>" nil t) (if (re-search-forward - (concat "\\(" prefix "\\w+\\)\\s-*\\($\\|(\\)") + (concat "\\_<\\(" prefix "\\w+\\)\\s-*\\($\\|(\\)") (matlab-point-at-eol) t) (setq lst (cons (match-string 1) lst)))) (nreverse lst))) @@ -426,7 +463,33 @@ Use `completion-in-region' to support the completion behavior." (completion-in-region common-substr-start-pt common-substr-end-pt completions) )) ) - + +(defun matlab--complete-compute-search-functions (semantics) + "Return the search functions for context specified by SEMATNICS." + (cond ((eq semantics 'solo) + '(matlab-solo-completions + matlab-find-user-functions + matlab-find-recent-variable)) + ((eq semantics 'boolean) + '(matlab-find-recent-variable + matlab-boolean-completions + matlab-find-user-functions + matlab-value-completions)) + ((eq semantics 'value) + '(matlab-find-recent-variable + matlab-find-user-functions + matlab-value-completions + matlab-boolean-completions)) + ((eq semantics 'property) + '(matlab-property-completions + matlab-find-user-functions + matlab-find-recent-variable + matlab-value-completions)) + (t '(matlab-find-recent-variable + matlab-find-user-functions + matlab-value-completions + matlab-boolean-completions)))) + (defun matlab-complete-symbol-local (&optional arg) "Complete a partially typed symbol in a MATLAB mode buffer. If the previously entered command was also `matlab-complete-symbol' @@ -459,29 +522,7 @@ to change it temporarily." (setq matlab-last-prefix prefix matlab-last-semantic sem matlab-completion-search-state - (cond ((eq sem 'solo) - '(matlab-solo-completions - matlab-find-user-functions - matlab-find-recent-variable)) - ((eq sem 'boolean) - '(matlab-find-recent-variable - matlab-boolean-completions - matlab-find-user-functions - matlab-value-completions)) - ((eq sem 'value) - '(matlab-find-recent-variable - matlab-find-user-functions - matlab-value-completions - matlab-boolean-completions)) - ((eq sem 'property) - '(matlab-property-completions - matlab-find-user-functions - matlab-find-recent-variable - matlab-value-completions)) - (t '(matlab-find-recent-variable - matlab-find-user-functions - matlab-value-completions - matlab-boolean-completions))))) + (matlab--complete-compute-search-functions sem))) (cond ((eq matlab-completion-technique 'increment) (let ((r nil) (donext (eq last-command 'matlab-complete-symbol))) diff --git a/matlab-maint.el b/matlab-maint.el index 1658219..ee6b071 100644 --- a/matlab-maint.el +++ b/matlab-maint.el @@ -24,6 +24,8 @@ (require 'matlab) (require 'matlab-shell) (require 'matlab-netshell) +(require 'semantic/symref) +(require 'semantic/symref/list) ;;; Code: @@ -31,8 +33,14 @@ ;; (defvar matlab-maint-mode-map (let ((km (make-sparse-keymap))) + ;; compile debug cycle (define-key km [f8] 'matlab-maint-run-tests) (define-key km [f9] 'matlab-maint-compile-matlab-emacs) + (define-key km [f10] 'matlab-maint-reload-mode) + ;; coding + (define-key km [f7] 'matlab-maint-symref-this) + ;; matlab + (define-key km [f5] 'matlab-maint-show-info) km) "Keymap used by matlab mode maintainers.") @@ -67,7 +75,35 @@ (matlab-maint-minor-mode 1)))) ) -;;; Commands +;;; Testing stuff in M buffers +;; +(defun matlab-maint-show-info () + "Show info about line in current matlab buffer." + (interactive) + (when (eq major-mode 'matlab-mode) + (matlab-show-line-info) + )) + +;;; HACKING ELISP +;; + +(defun matlab-maint-symref-this () + "Open a symref buffer on symbol under cursor." + (interactive) + (save-buffer) + (semantic-fetch-tags) + (let ((ct (semantic-current-tag))) + ;; Must have a tag... + (when (not ct) (error "Place cursor inside tag to be searched for")) + ;; Gather results and tags + (message "Gathering References for %s.." (semantic-tag-name ct)) + (let* ((name (semantic-tag-name ct)) + (res (semantic-symref-find-references-by-name name))) + (semantic-symref-produce-list-on-results res name) + (semantic-symref-list-expand-all) ))) + + +;;; COMPILE AND TEST ;; ;; Helpful commands for maintainers. (defcustom matlab-maint-compile-opts '("emacs" "emacs24" "emacs25" "emacs26") @@ -112,6 +148,8 @@ "Run the tests for matlab mode. With universal ARG, ask for the code to be run with output tracking turned on." (interactive "P") + (when (buffer-file-name) + (save-buffer)) (save-excursion (matlab-maint-set-buffer-to "tests/Makefile") (if (or arg matlab-shell-io-testing) @@ -123,6 +161,21 @@ With universal ARG, ask for the code to be run with output tracking turned on." (delete-other-windows) (goto-char (point-max))) +(defun matlab-maint-reload-mode () + "Reload matlab mode, and refresh displayed ML buffers modes." + (interactive) + (load-library "matlab-syntax") + (load-library "matlab-scan") + (load-library "matlab") + (mapc (lambda (b) (with-current-buffer b + (when (eq major-mode 'matlab-mode) + (message "Updating matlab mode in %S" b) + (matlab-mode)))) + (buffer-list (selected-frame))) + (message "loading done") + ) +;;; MATLAB SHELL tools +;; (defun matlab-maint-set-buffer-to (file) "Set the current buffer to FILE found in matlab-mode's source. diff --git a/matlab-scan.el b/matlab-scan.el new file mode 100644 index 0000000..51b02dc --- /dev/null +++ b/matlab-scan.el @@ -0,0 +1,1476 @@ +;;; matlab-scan.el --- Tools for contextually scanning a MATLAB buffer +;; +;; Copyright (C) 2021 Eric Ludlam +;; +;; Author: +;; +;; 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 of the +;; License, 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 this program. If not, see https://www.gnu.org/licenses/. + +;;; Commentary: +;; +;; Handle all the scanning and computing for MATLAB files. +;; +;; * Regular expressions for finding different kinds of syntax +;; * Systems for detecting what is on a line +;; * Systems for computing indentation + +(require 'matlab-syntax) + +;;; Code: + + +;;; Keyword and REGEX constants +;; +;; List of our keywords, and tools to look up keywords and find out +;; what they are. +(defconst matlab-block-keyword-list '(("end" . end) + ("function" . decl) + ("classdef" . decl) + ("arguments" . args) + ("properties" . mcos) + ("methods" . mcos) + ("events" . mcos) + ("enumeration" . mcos) + ("if" . ctrl) + ("elseif" . mid) + ("else" . mid) + ("ifelse" . mid) + ("for" . ctrl) + ("parfor" . ctrl) + ("while" . ctrl) + ("spmd" . ctrl) + ("switch" . ctrl) + ("case" . case) + ("otherwise" . case) + ("try" . ctrl) + ("catch" . mid) + ("break" . keyword) + ("continue" . keyword) + ("return" . keyword) + ("global" . vardecl) + ("persistent" . vardecl) + ) + "List of keywords that are part of code blocks.") + +(defconst matlab-keyword-table + (let ((ans (matlab-obarray-make 23))) + (mapc (lambda (elt) (set (intern (car elt) ans) (cdr elt))) + matlab-block-keyword-list) + ans) + "Keyword table for fast lookups of different keywords and their purpose.") + +(defun matlab-keyword-p (word) + "Non nil if WORD is a keyword. +If word is a number, it is a match-string index for the current buffer." + (let* ((local-word (if (numberp word) + (match-string-no-properties word) + word)) + (sym (intern-soft local-word matlab-keyword-table))) + (and sym (symbol-value sym)))) + +(defsubst matlab-on-keyword-p () + "Return the type of keyword under point, or nil." + (when (matlab-valid-keyword-syntax) + ;; Not in invalid context, look it up. + (matlab-keyword-p (buffer-substring-no-properties + (save-excursion (skip-syntax-backward "w_") (point)) + (save-excursion (skip-syntax-forward "w_") (point)))))) + +(defvar matlab-kwt-all nil) +(defvar matlab-kwt-decl nil) +(defvar matlab-kwt-indent nil) +(defvar matlab-kwt-end nil) +(defvar matlab-kwt-blocks nil) +(defvar matlab-kwt-mcos nil) +(defvar matlab-kwt-args nil) +(defvar matlab-kwt-vardecl nil) +(defvar matlab-kwt-fl-simple nil) + +(defun matlab-keyword-regex (types) + "Find keywords that match TYPES and return optimized regexp. +TYPES can also be a single symbol that represents a common list of +keyword types. These include: + all - any kind of keyword + decl - declarations, like class or function + indent - any keyword that causes an indent + end - the end keyword (that causes dedent) + blocks - indent and end keywords + fl-simple - simple to highilght w/ font lock +Caches some found regexp to retrieve them faster." + (cond + ((or (eq types nil) (eq types 'all)) + (or matlab-kwt-all (setq matlab-kwt-all (matlab--keyword-regex nil)))) + ((eq types 'decl) + (or matlab-kwt-decl (setq matlab-kwt-decl (matlab--keyword-regex '(decl))))) + ((eq types 'indent) + (or matlab-kwt-indent (setq matlab-kwt-indent (matlab--keyword-regex '(decl ctrl args mcos))))) + ((eq types 'end) + (or matlab-kwt-end (setq matlab-kwt-end (matlab--keyword-regex '(end))))) + ((eq types 'blocks) + (or matlab-kwt-blocks (setq matlab-kwt-blocks (matlab--keyword-regex '(end decl ctrl args mcos))))) + ((eq types 'fl-simple) + (or matlab-kwt-fl-simple (setq matlab-kwt-fl-simple (matlab--keyword-regex '(end decl ctrl mid case keyword vardecl))))) + ((eq types 'mcos) + (or matlab-kwt-mcos (setq matlab-kwt-mcos (matlab--keyword-regex '(mcos))))) + ((eq types 'args) + (or matlab-kwt-args (setq matlab-kwt-args (matlab--keyword-regex '(args))))) + ((eq types 'vardecl) + (or matlab-kwt-vardecl (setq matlab-kwt-vardecl (matlab--keyword-regex '(vardecl))))) + ((symbolp types) + (matlab--keyword-regex (list types))) + (t + (matlab--keyword-regex types)))) + +(defun matlab--keyword-regex (types) + "Find keywords that match TYPES and return an optimized regexp." + (let ((lst nil)) + (mapc (lambda (C) (when (or (null types) (memq (cdr C) types)) (push (car C) lst))) + matlab-block-keyword-list) + (regexp-opt lst 'symbols))) + + +;;; Context Parsing +;; +;; Find some fast ways to identify the context for a given line. +;; Use tricks to derive multiple pieces of information and do lookups +;; as quickly as possible. + +(defun matlab-compute-line-context (level &rest context) + "Compute and return the line context for the current line of MATLAB code. +LEVEL indicates how much information to return. +LEVEL of 1 is the most primitive / simplest data. +LEVEL of 2 is stuff that is derived from previous lines of code. +This function caches computed context onto the line it is for, and will +return the cache if it finds it." + (save-match-data + (cond ((= level 1) + (let ((ctxt (save-excursion + (back-to-indentation) + (matlab-scan-cache-get)))) + (unless ctxt + (setq ctxt (matlab-compute-line-context-lvl-1)) + (matlab-scan-cache-put ctxt)) + ctxt)) + ((= level 2) + (apply 'matlab-compute-line-context-lvl-2 context)) + (t + nil))) + ) + +;;; LEVEL 1 SCANNER +;; +;; This scanner pulls out context available on the current line. +;; This inludes the nature of the line, and anything sytax-ppss gives is. +(defconst mlf-ltype 0) +(defconst mlf-stype 1) +(defconst mlf-point 2) +(defconst mlf-indent 3) +(defconst mlf-entity-start 4) +(defconst mlf-paren-depth 5) +(defconst mlf-paren-inner-char 6) +(defconst mlf-paren-inner-col 7) +(defconst mlf-paren-inner-point 8) +(defconst mlf-paren-outer-char 9) +(defconst mlf-paren-outer-point 10) +(defconst mlf-paren-delta 11) +(defconst mlf-end-comment-type 12) +(defconst mlf-end-comment-pt 13) + +(defun matlab-compute-line-context-lvl-1 () + "Compute and return the level1 context for the current line of MATLAB code. +Level 1 contexts are things quickly derived from `syntax-ppss' +and other simple states. +Computes multiple styles of line by checking for multiple types of context +in a single call using fastest methods." + (save-excursion + ;; About use of `syntax-ppss' - This util has a 1 element cache, + ;; and can utilize the cache as it parses FORWARD only. Tools + ;; like `back-to-indentation' also needs to propertize the buffer + ;; since that uses syntax elements to do it's work. To make this + ;; fcn faster, we call `syntax-ppss' once on BOL, and then + ;; parse-partial-sexp for other locations as a way to boost the + ;; speed of calls to `syntax-ppss' that come later. + + ;; Additional note: `back-to-indentation' used below calls + ;; syntax-propertize. Command dual needs to call + ;; `syntax-ppss' which it does on bol By setting `syntax-ppss' + ;; internal cache on bol, in cases where propertize needs to + ;; be called, the cache returns immediatly during the + ;; propertize, and needs to do no extra work. + (beginning-of-line) + (let* ((ppsbol (syntax-ppss (point))) ;; Use the cache + (pps (progn (back-to-indentation) + ;; Compute by hand - leaving cache alone. + (parse-partial-sexp (point-at-bol) + (point) + nil nil + ;; previous state + ppsbol))) + (ppsend (save-excursion + ;; Starting @ indentation, parse forward to eol + ;; and see where we are. + ;; Compute by hand, leaving cache alone. + (parse-partial-sexp (point) (point-at-eol) + nil nil + ;; Previous state + pps))) + (ltype 'empty) + (stype nil) + (pt (point)) + (indent (current-indentation)) + (start (point)) + (paren-depth (nth 0 pps)) + (paren-inner-char nil) + (paren-inner-col nil) + (paren-inner-point nil) + (paren-outer-char nil) + (paren-outer-point nil) + (paren-delta (- (car pps) (car ppsend))) + (ec-type nil) + (ec-col nil) + (cont-from-prev nil) + (symval nil) + ) + + ;; This means we are somewhere inside a cell, array, or arg list. + ;; Find out the kind of list we are in. + ;; Being in a multi-line list is valid for all other states like + ;; empty lines, and block comments + (when (> (nth 0 pps) 0) + (save-excursion + (goto-char (car (last (nth 9 pps)))) + (setq paren-inner-char (char-after (point)) + paren-inner-col (current-column) + paren-inner-point (point) + paren-outer-point (car (nth 9 pps)) + paren-outer-char (char-after paren-outer-point) ))) + + (cond + ;; For comments - We can only ever be inside a block comment, so + ;; check for that. + ;; 4 is comment flag. 7 is '2' if block comment + ((and (nth 4 pps) (eq (nth 7 pps) 2)) + (setq ltype 'comment + stype (cond ((looking-at "%}\\s-*$") + 'block-end) + ((looking-at "%") + 'block-body-prefix) + (t 'block-body)) + start (nth 8 pps))) + + ;; If indentation lands on end of line, this is an empty line + ;; so nothing left to do. Keep after block-comment-body check + ;; since empty lines in a block comment are valid. + ((eolp) + nil) + + ;; Looking at a % means one of the various comment flavors. + ((eq (char-after (point)) ?\%) + (setq ltype 'comment + stype (cond ((looking-at "%{\\s-*$") + 'block-start) + ((looking-at "%%") + 'cell-start) + ;; The %^ is used in tests + ((looking-at "%\\(?:\\^\\| \\$\\$\\$\\)") + 'indent-ignore) + (t nil)))) + + ;; Looking at word constituent. If so, identify if it is one of our + ;; special identifiers. + ((and (= paren-depth 0) + (setq symval (matlab-on-keyword-p))) + + (cond + ;; Special end keyword is in a class all it's own + ((eq symval 'end) + (setq ltype 'end)) + ;; If we found this in our keyword table, then it is a start + ;; of a block with a subtype. + ((memq symval '(decl args mcos ctrl mid case)) + (setq ltype 'block-start + stype symval)) + ;; Some keywords aren't related to blocks with indentation + ;; controls. Those are treated as code, with a type. + (t + (setq ltype 'code + stype symval)) + )) + + ;; Looking at a close paren. + ((and (< 0 paren-depth) (looking-at "\\s)")) + (setq ltype 'close-paren)) + + ;; Last stand - drop in 'code' to say - yea, just some code. + (t (setq ltype 'code)) + + ) + + ;; NEXT - Check context at the end of this line, and determine special + ;; stuff about it. + + ;; When the line ends with a comment. + ;; Also tells us about continuations and comment start for lining up tail comments. + (let ((csc (nth 8 ppsend))) + (when (and (not csc) (eq stype 'block-end)) + ;; block comment end lines must end in a comment, so record + ;; the beginning of that block comment instead. + (setq csc (nth 8 pps)) ) + + ;; If we have something, record what it is. + (when csc + (setq ec-col csc + ec-type (cond ((= (char-after csc) ?\%) 'comment) + ((= (char-after csc) ?\.) 'ellipsis) + (t 'commanddual))) + )) + + (list ltype stype pt indent start paren-depth + paren-inner-char paren-inner-col paren-inner-point + paren-outer-char paren-outer-point paren-delta + ec-type ec-col + ;;cont-from-prev + ) + ))) + +;;; Accessor Utilities for LEVEL 1 +;; +;; Use these to query a context for a piece of data +(defmacro matlab-with-context-line (__context &rest forms) + "Save excursion, and move point to the line specified by CONTEXT. +Takes a lvl1 or lvl2 context. +Returns the value from the last part of forms." + (declare (indent 1) (debug (form &rest form))) + `(save-excursion + ;; The CAR of a LVL2 is a LVL1. If __context is LVL1, then + ;; the car-safe will return nil + (goto-char (nth mlf-point (if (consp (car ,__context)) + (car ,__context) + ,__context))) + ,@forms)) + +(defsubst matlab-line-point (lvl1) + "Return the point at beginning of indentation for line specified by lvl1." + (nth mlf-point lvl1)) + +(defsubst matlab-line-indentation (lvl1) + "Return the indentation of the line specified by LVL1." + (nth mlf-indent lvl1)) + +(defsubst matlab-line-empty-p (lvl1) + "Return t if the current line is empty based on LVL1 cache." + (eq (car lvl1) 'empty)) + +;; Comments +(defsubst matlab-line-comment-p (lvl1) + "Return t if the current line is a comment." + (eq (car lvl1) 'comment)) + +(defsubst matlab-line-regular-comment-p (lvl1) + "Return t if the current line is a comment w/ no attributes." + (and (eq (car lvl1) 'comment) (eq (nth 1 lvl1) nil))) + +(defsubst matlab-line-comment-ignore-p (lvl1) + "Return t if the current line is an indentation ignored comment." + (and (matlab-line-comment-p lvl1) (eq (nth mlf-stype lvl1) 'indent-ignore))) + +(defsubst matlab-line-comment-style (lvl1) + "Return type type of comment on this line." + (and (matlab-line-comment-p lvl1) (nth mlf-stype lvl1))) + +(defsubst matlab-line-end-comment-column (lvl1) + "Return column of comment on line, or nil if no comment. +All lines that start with a comment end with a comment." + (when (eq (nth mlf-end-comment-type lvl1) 'comment) + (save-excursion + ;; NOTE: if line is in a block comment, this end pt is + ;; really the beginning of the block comment. + (goto-char (nth mlf-end-comment-pt lvl1)) + (current-column)))) + +(defsubst matlab-line-end-comment-point (lvl1) + "Return star of comment on line, or nil if no comment. +All lines that start with a comment end with a comment." + (when (eq (nth mlf-end-comment-type lvl1) 'comment) + (nth mlf-end-comment-pt lvl1))) + +(defsubst matlab-line-ellipsis-p (lvl1) + "Return if this line ends with a comment." + (eq (nth mlf-end-comment-type lvl1) 'ellipsis)) + +(defsubst matlab-line-commanddual-p (lvl1) + "Return if this line ends with command duality string." + (eq (nth mlf-end-comment-type lvl1) 'commanddual)) + +(defsubst matlab-line-block-comment-start (lvl1) + "Return the start of the block comment we are in, or nil." + (when (and (matlab-line-comment-p lvl1) + (memq (nth mlf-stype lvl1) + '(block-start block-end block-body block-body-prefix))) + (nth mlf-entity-start lvl1))) + +;; Code and Declarations +(defsubst matlab-line-code-p (lvl1) + "Return t if the current line is code." + (eq (car lvl1) 'code)) + +(defsubst matlab-line-boring-code-p (lvl1) + "Return t if the current line is code w/ no keyword." + (and (eq (car lvl1) 'code) (not (nth 1 lvl1)))) + +(defsubst matlab-line-block-start-keyword-p (lvl1) + "Return t if the current line starts with block keyword." + (eq (car lvl1) 'block-start)) + +(defsubst matlab-line-declaration-p (lvl1) + "If the current line is a declaration return non-nil. +Declarations are things like function or classdef." + (and (matlab-line-block-start-keyword-p lvl1) (eq (nth mlf-stype lvl1) 'decl))) + +(defsubst matlab-line-end-p (lvl1) + "Non nil If the current line starts with an end." + (eq (car lvl1) 'end)) + +(defsubst matlab-line-block-middle-p (lvl1) + "Non nil If the current line starts with a middle block keyword. +These are keywords like `else' or `catch'." + (and (eq (car lvl1) 'block-start) (eq (nth 1 lvl1) 'mid))) + +(defsubst matlab-line-block-case-p (lvl1) + "Non nil If the current line starts with a middle block keyword. +These are keywords like `else' or `catch'." + (and (eq (car lvl1) 'block-start) (eq (nth 1 lvl1) 'case))) + +(defun matlab-line-end-of-code (&optional lvl1) + "Go to the end of the code on the current line. +If there is a comment or ellipsis, go to the beginning of that. +If the line starts with a comment return nil, otherwise t." + (unless lvl1 (setq lvl1 (matlab-compute-line-context 1))) + (goto-char (nth mlf-point lvl1)) + (if (or (matlab-line-empty-p lvl1) (matlab-line-comment-p lvl1)) + nil + ;; Otherwise, look for that code. + (if (eq (nth mlf-end-comment-type lvl1) 'comment) + (goto-char (nth mlf-end-comment-pt lvl1)) + (goto-char (point-at-eol))))) + +(defun matlab-line-end-of-code-needs-semicolon-p (&optional lvl1) + "Return non-nil of this line of code needs a semicolon. +Move cursor to where the ; should be inserted. +Return nil for empty and comment only lines." + (unless lvl1 (setq lvl1 (matlab-compute-line-context 1))) + (let ((endpt nil)) + (save-excursion + (when (and (not (matlab-beginning-of-outer-list)) + (matlab-line-boring-code-p lvl1) + (matlab-line-end-of-code lvl1)) + (skip-syntax-backward " ") + (when (and (not (matlab-cursor-in-string-or-comment)) + (not (= (preceding-char) ?\;))) + (setq endpt (point))) + )) + (when endpt + (goto-char endpt)))) + +(defun matlab-line-first-word-text (&optional lvl1) + "Return text for the specific keyword found under point." + (matlab-with-context-line lvl1 + (buffer-substring-no-properties + (point) (save-excursion (skip-syntax-forward "w_") (point)))) + ) + +;; Declarations and Names +(defvar matlab-fl-opt-whitespace) +(defun matlab-line-declaration-name (&optional lvl1) + "Return string name of a declaration on the line. +For functions, this is the name of the function. +For classes, this is the name of the class. +Output is a list of the form: + ( NAME DECL-TYPE START END) +Where NAME is the name, and DECL-TYPE is one of +'function or 'class +START and END are buffer locations around the found name. +If the current line is not a declaration, return nil." + (unless lvl1 (setq lvl1 (matlab-compute-line-context 1))) + (when (matlab-line-declaration-p lvl1) + (let (type name start end) + (matlab-navigation-syntax + (matlab-with-context-line lvl1 + (cond + ;; FUNCTION : can have return arguments that need to be skipped. + ((string= (matlab-line-first-word-text lvl1) "function") + (forward-word 1) + (skip-syntax-forward " ") + (forward-comment 10) + (when (looking-at (concat + "\\(\\[[^]]+]\\|\\w+\\)" + matlab-fl-opt-whitespace + "=")) + (goto-char (match-end 0)) + (skip-syntax-forward " ") + (forward-comment 10) + ) + (setq type 'function + start (point) + end (save-excursion + (skip-syntax-forward "w_") + (point)))) + + ;; CLASS : Can have class attributes to be skipped. + ((string= (matlab-line-first-word-text lvl1) "classdef") + (forward-word 1) + (skip-syntax-forward " ") + (forward-comment 10) + (when (looking-at "([^)]+)") + (goto-char (match-end 0)) + (skip-syntax-forward " ") + (forward-comment 10) + ) + (setq type 'classdef + start (point) + end (save-excursion + (skip-syntax-forward "w_") + (point))))))) + + (when type + (list type (buffer-substring-no-properties start end) + start end)) + ))) + +;; Parenthetical blocks +(defsubst matlab-line-close-paren-p (lvl1) + "Non nil If the current line starts with closing paren (any type.)" + (eq (car lvl1) 'close-paren)) + +(defsubst matlab-line-paren-depth (lvl1) + "The current depth of parens at the start of this line" + (nth mlf-paren-depth lvl1)) + +(defsubst matlab-line-close-paren-inner-char (lvl1) + "Return the paren character for the parenthetical expression LVL1 is in." + (nth mlf-paren-inner-char lvl1)) + +(defsubst matlab-line-close-paren-inner-col (lvl1) + "Return the paren column for the prenthetical expression LVL1 is in." + (nth mlf-paren-inner-col lvl1)) + +(defsubst matlab-line-close-paren-inner-point (lvl1) + "Return the paren column for the prenthetical expression LVL1 is in." + (nth mlf-paren-inner-point lvl1)) + +(defsubst matlab-line-close-paren-outer-char (lvl1) + "The paren character for the outermost prenthetical expression LVL1 is in." + (nth mlf-paren-outer-char lvl1)) + +(defsubst matlab-line-close-paren-outer-point (lvl1) + "The point the outermost parenthetical expression start is at." + (nth mlf-paren-outer-point lvl1)) + + +;;; LEVEL 2 SCANNER +;; +;; This scanner extracts information that affects the NEXT line of ML code. +;; This inludes things like ellipsis and keyword chains like if/end blocks. +;; +;; Level 2 scanning information cascades from line-to-line, several fields will +;; be blank unless a previous line is also scanned. +(defconst mlf-level1 0) +(defconst mlf-previous-command-beginning 1) +(defconst mlf-previous-nonempty 2) +(defconst mlf-previous-code 3) +(defconst mlf-previous-block 4) +(defconst mlf-previous-fcn 5) + +(defun matlab-compute-line-context-lvl-2 (&optional lvl1 previous2) + "Compute the level 2 context for the current line of MATLAB code. +Level 2 context are things that are derived from previous lines of code. + +Some of that context is derived from the LVL1 context such as paren depth, +and some scaning previous lines of code. + +LVL1 will be computed if not provided. + +This function will generate a mostly empty structure, and will +fill in context from the PREVIOUS2 input as needed. Empty stats +will be computed by accessors on an as needed basis. If PREVIOUS +2 is not provided, it will go back 1 line, scan it for lvl1 data +and use that. + +Empty stats are filled in as t. nil means there is no context, or +a lvl1 stat block for the line with that meaning. + +The returned LVL2 structure will fill out to be a chain of all previous +LVL2 outputs up to a context break. The chains will be summarized in slots +in the returned list for quick access." + (when (not lvl1) (setq lvl1 (matlab-compute-line-context-lvl-1))) + + (save-excursion + (let ((prev-lvl1 t) + (prev-lvl2 t) + (prev-cmd-begin t) + (prev-nonempty t) + (prev-code1 t) + (prev-block1 t) + (prev-fcn1 t) + (tmp nil) + ) + + ;; copy data from previous2. + (if previous2 + (progn + (setq prev-lvl1 t + prev-lvl2 t + prev-cmd-begin (nth mlf-previous-command-beginning previous2) + prev-nonempty (nth mlf-previous-nonempty previous2) + prev-code1 (nth mlf-previous-code previous2) + prev-block1 (nth mlf-previous-block previous2) + prev-fcn1 (nth mlf-previous-fcn previous2) + ) + (matlab-scan-stat-inc 'prev2) + ) + ;; Else - previous LVL1 is the one thing we'll compute since we need to + ;; init our trackers. + (save-excursion + (beginning-of-line) + (if (bobp) + (setq prev-lvl1 nil) + (forward-char -1) + (setq prev-lvl1 (matlab-compute-line-context 1)) + (matlab-scan-stat-inc 'prev2miss) + ))) + + ;; prev line can be nil if at beginning of buffer. + (if (not prev-lvl1) + (setq prev-lvl2 nil + prev-cmd-begin nil + prev-nonempty nil + prev-code1 nil + prev-block1 nil + prev-fcn1 nil + ) + + ;; If we do have a previous lvl1, then we can compute from it. + ;; Override parts of our data based on prev-lvl2 which might + ;; be one of the things we care about. + (when (not (matlab-line-empty-p prev-lvl1)) + ;; Our prev line was non empty, so remember. + (setq prev-nonempty prev-lvl1) + + (if (not (memq (car prev-lvl1) '(empty comment))) + (progn + ;; Our prev line wasn't a comment or empty, so remember. + (setq prev-code1 prev-lvl1) + + (when (and (= (matlab-line-paren-depth prev-lvl1) 0) + (not (matlab-line-ellipsis-p prev-lvl1))) + ;; Our previous line some code, but was not in an + ;; array, nor an ellipse. Thus, reset beginning of cmd + ;; to this line we are on. + (setq prev-cmd-begin lvl1) + ) + + (when (eq (car prev-lvl1) 'block-start) + ;; We have a block start, so remember + (setq prev-block1 prev-lvl1) + + (when (eq (nth mlf-stype prev-lvl1) 'decl) + (setq prev-fcn1 prev-lvl1)) + )) + + ;; Do something with comment here ?? + ))) + + (list lvl1 prev-cmd-begin prev-nonempty prev-code1 prev-block1 prev-fcn1 + + )))) + +;;; Refresh this lvl2 +;; +(defun matlab-refresh-line-context-lvl2 (lvl2 &optional lvl1) + "Refresh the content of this lvl2 context. +Assume ONLY the line this lvl2 context belongs to has changed +and we don't have any caches in later lines." + (matlab-with-context-line lvl2 + (when (not lvl1) (setq lvl1 (matlab-compute-line-context 1))) + ;; cmd begin can be same as self. Check and replace + (when (eq (car lvl2) (nth mlf-previous-command-beginning lvl2)) + (setcdr (nthcdr mlf-previous-command-beginning lvl2) lvl1)) + ;; Replace self. + (setcar lvl2 lvl1) + )) + +;;; Simple Accessors +;; +(defun matlab-get-lvl1-from-lvl2 (lvl2) + "Return a LVL1 context. +If input LVL2 is a level 2 context, return the lvl1 from it. +If the input is a lvl1, then return that. +If LVL2 is nil, compute it." + (if lvl2 + (if (consp (car lvl2)) (car lvl2) lvl2) + (matlab-compute-line-context 1))) + +(defun matlab-previous-nonempty-line (lvl2) + "Return lvl1 ctxt for previous non-empty line." + (let ((prev (nth mlf-previous-nonempty lvl2)) + ) + (if (eq prev t) + ;; Compute it and stash it. + (save-excursion + (matlab-scan-stat-inc 'nonemptymiss) + (beginning-of-line) + (skip-syntax-backward " >") ;; skip spaces, and newlines w/ comment end on it. + ;; TODO - this stops on ignore comments. + (setq prev (matlab-compute-line-context 1)) + (setcar (nthcdr mlf-previous-nonempty lvl2) prev)) + ;; else record a cache hit + (matlab-scan-stat-inc 'nonempty) + ) + prev)) + +(defun matlab-previous-code-line (lvl2) + "Return lvl1 ctxt for previous non-empty line." + (let ((prev (nth mlf-previous-code lvl2)) + ) + (if (eq prev t) + ;; Compute it and stash it. + (save-excursion + (matlab-scan-stat-inc 'codemiss) + (beginning-of-line) + (when (not (bobp)) + ;; Skip over all comments and intervening whitespace + (forward-comment -100000) + ;; If no comments, then also skip over these whitespace chars. + (skip-chars-backward " \t\n") + (setq prev (matlab-compute-line-context 1)) + (setcar (nthcdr mlf-previous-code lvl2) prev))) + ;; else record a cache hit + (matlab-scan-stat-inc 'code) + ) + prev)) + +(defun matlab-previous-command-begin (lvl2) + "Return lvl1 ctxt for previous non-empty line." + (let ((prev (nth mlf-previous-command-beginning lvl2)) + ) + (if (eq prev t) + ;; Compute it and stash it. + (save-excursion + (matlab-with-context-line (matlab-previous-code-line lvl2) + (matlab-scan-beginning-of-command) + (setq prev (matlab-compute-line-context 1)) + (setcar (nthcdr mlf-previous-command-beginning lvl2) prev))) + ;; else record a cache hit + (matlab-scan-stat-inc 'cmdbegin) + ) + prev)) + + +;;; Scanning Accessor utilities +;; +;; Some utilities require some level of buffer scanning to get the answer. +;; Keep those separate so they can depend on the earlier decls. +(defun matlab-scan-comment-help-p (ctxt &optional pt) + "Return declaration column if the current line is part of a help comment. +Use the context CTXT as a lvl1 or lvl2 context to compute. +Declarations are things like functions and classdefs. +Indentation a help comment depends on the column of the declaration. +Optional PT, if non-nil, means return the point instead of column" + (let ((lvl2 nil) (lvl1 nil)) + (if (symbolp (car ctxt)) + (setq lvl1 ctxt) + (setq lvl1 (matlab-get-lvl1-from-lvl2 ctxt) + lvl2 ctxt)) + + (when (matlab-line-comment-p lvl1) + ;; Try to get from lvl2 context + (let ((c-lvl1 (when lvl2 (matlab-previous-code-line lvl2))) + (boc-lvl1 nil)) + (save-excursion + (unless c-lvl1 + ;; If not, compute it ourselves. + (save-excursion + (beginning-of-line) + (forward-comment -100000) + (setq c-lvl1 (matlab-compute-line-context 1)))) + ;; On previous line, move to beginning of that command. + (matlab-scan-beginning-of-command c-lvl1 'code-only) + (setq boc-lvl1 (matlab-compute-line-context 1)) + ;; On previous code line - was it a declaration? + (when (matlab-line-declaration-p boc-lvl1) + (matlab-with-context-line boc-lvl1 + (if pt (point) (current-indentation))))))))) + +(defun matlab-scan-previous-line-ellipsis-p () + "Return the position of the previous line's continuation if there is one. +This is true iff the previous line has an ellipsis." + (save-excursion + (beginning-of-line) + (when (not (bobp)) + (forward-char -1) + ;; Ellipsis scan always resets at BOL, so call non-cached + ;; `parse-partial-sexp' instead of regular `syntax-ppss' which + ;; can be slow as it attempts to get a solid start from someplace + ;; potentially far away. + (let* ((pps (parse-partial-sexp (point-at-bol) (point-at-eol))) ;;(syntax-ppss (point))) + (csc (nth 8 pps))) + ;; Ellipsis start has a syntax of 11 (comment-start). + ;; Other comments have high-bit flags, so don't == 11. + (when (and csc (= (car (syntax-after csc)) 11)) + csc))))) + +(defun matlab-scan-beginning-of-command (&optional lvl1 code-only) + "Return point in buffer at the beginning of this command. +This function walks up any preceeding comments, enclosing parens, and skips +backward over lines that include ellipsis. +If optional CODE-ONLY is specified, it doesn't try to scan over +preceeding comments." + (unless lvl1 (setq lvl1 (matlab-compute-line-context 1))) + ;; If we are in a block comment, just jump to the beginning, and + ;; that's it. + (let ((bcs nil)) + + (goto-char (matlab-line-point lvl1)) + + (when (not code-only) + ;; Code only branch skips comments so the help-comment + ;; routine can also use this function. + (setq bcs (matlab-line-block-comment-start lvl1)) + (when bcs + (goto-char bcs) + (setq lvl1 (matlab-compute-line-context 1))) + + ;; If we are in a help comment, jump over that first. + (setq bcs (matlab-scan-comment-help-p lvl1 'point)) + (when bcs + (goto-char bcs) + (setq lvl1 (matlab-compute-line-context 1)))) + + ;; Now scan backward till we find the beginning. + (let ((found nil)) + (while (not found) + ;; first - just jump to our outermost point. + (goto-char (or (matlab-line-close-paren-outer-point lvl1) (point))) + ;; Second - is there an ellipsis on prev line? + (let ((prev (matlab-scan-previous-line-ellipsis-p))) + (if (not prev) + (setq found t) + ;; Move to prev location if not found. + (goto-char prev) + (setq lvl1 (matlab-compute-line-context 1)) + ))) + (back-to-indentation) + (point)))) + +(defun matlab-scan-end-of-command (&optional lvl1) + "Return point in buffer at the end of this command. +This function walks down past continuations and open arrays." + (unless lvl1 (setq lvl1 (matlab-compute-line-context 1))) + ;; If we are in a block comment, just jump to the end, and + ;; that's it. + (let ((bcs (matlab-line-block-comment-start lvl1))) + (if bcs + (progn + (goto-char bcs) + (forward-comment 1) + (setq lvl1 (matlab-compute-line-context 1))) + + (let ((done nil) + (lvlwalk lvl1)) + (while (not done) + (end-of-line) + (cond ((matlab-end-of-outer-list) + ;; If we are in a list, this moves to the end and + ;; returns non-nil. We are now where we want to be + ;; so nothing to do. + nil) + + ((matlab-line-ellipsis-p lvlwalk) + ;; This is a continuation, keep going. + (forward-line 1)) + + (t + ;; None of these conditions were true, so + ;; we must be done! + (setq done t)) + ) + + ;; Protect against travelling too far. + (when (eobp) (setq done t)) + ;; Scan the next line. + (when (not done) (setq lvlwalk (matlab-compute-line-context 1))) + )) + + ;; Return where we ended up + (end-of-line) + (point-at-eol)))) + + +;;; BLOCK SCANNING and SEARCHING +;; +;; Focused scanning across block structures, like if / else / end. + +(defvar matlab-functions-have-end) +(defvar matlab-indent-level) + +(defsubst matlab--mk-keyword-node () + "Like `matlab-on-keyword-p', but returns a node for block scanning. +The elements of the return node are: + 0 - type of keyword, like ctrl or decl + 1 - text of the keyword + 2 - buffer pos for start of keyword + 3 - buffer pos for end of keyword" + ;; Don't check the context - assume our callers have vetted this + ;; point. This is b/c the search fcns already skip comments and + ;; strings for efficiency. + (let* ((start (save-excursion (skip-syntax-backward "w_") (point))) + (end (save-excursion (skip-syntax-forward "w_") (point))) + (txt (buffer-substring-no-properties start end)) + (type (matlab-keyword-p txt))) + (when type (list type txt start end) ))) + +;;; Valid keyword locations +;; +(defsubst matlab--valid-keyword-point () + "Return non-nil if point is valid for keyword. +Returns nil for failing `matlab-valid-keyword-syntax'. +Returns nil if preceeding non-whitespace char is `.'" + (and (matlab-valid-keyword-syntax) + (not (matlab-syntax-keyword-as-variable-p)))) + +(defsubst matlab--known-parent-block(parent) + "Return PARENT if it is a known parent block." + (if (or (not parent) (eq (car parent) 'unknown)) + nil + parent)) + +(defun matlab--valid-keyword-node (&optional node parentblock) + "Return non-nil if NODE is in a valid location. +Optional parentblock specifies containing parent block if it is known." + (when (not node) (setq node (matlab--mk-keyword-node))) + (when node + (save-excursion + (goto-char (nth 2 node)) + (and (matlab--valid-keyword-point) + (cond ((eq (car node) 'mcos) + (matlab--valid-mcos-keyword-point parentblock)) + ((eq (car node) 'args) + (matlab--valid-arguments-keyword-point parentblock)) + (t t)))))) + +(defun matlab--valid-mcos-keyword-point (&optional parentblock) + "Return non-nil if at a location that is valid for MCOS keywords. +This means that the parent block is a classdef. +Optional input PARENTBLOCK is a precomputed keyword node +representing the current block context point is in. +Assume basic keyword checks have already been done." + (and + ;; Must be first thing on line - before checking other stuff. + (save-excursion (skip-syntax-backward "w") (skip-syntax-backward " ") (bolp)) + + ;; If a parent was provided, use that. + (if (matlab--known-parent-block parentblock) + (string= (nth 1 parentblock) "classdef") + + ;; else more expensive check + (and (eq matlab-functions-have-end 'class) ;; not a class, no mcos allowed. + ;; Otherwise, roll back a single command. in MUST be + ;; an END indent 4 or CLASSDEF + (save-excursion + (skip-syntax-backward "w") + (forward-comment -100000) + (matlab-scan-beginning-of-command) + (let ((prev (matlab--mk-keyword-node))) + (or (string= (nth 1 prev) "classdef") + (and (string= (nth 1 prev) "end") + (= (current-indentation) matlab-indent-level))))))))) + +(defun matlab--valid-arguments-keyword-point (&optional parentblock) + "Return non-nil if at a location that is valid for ARGUMENTS keyword. +This means that the parent block is a function, and this is first cmd in +the function. +Optional input PARENTBLOCK is a precomputed keyword node +representing the current block context point is in. +Assume basic keyword checks have already been done." + (save-excursion + (skip-syntax-backward "w") + (and + ;; Must be first thing on line - before checking other stuff. + (save-excursion (skip-syntax-backward "w") (skip-syntax-backward " ") (bolp)) + ;; More expensive checks + (let ((parent + (or (matlab--known-parent-block parentblock) ;; technically this can lie, but it's fast. + (save-excursion (forward-comment -100000) + (matlab-scan-beginning-of-command) + (and (matlab--valid-keyword-point) + (matlab--mk-keyword-node)))))) + (and parent + (or (string= (nth 1 parent) "function") + ;; If not a function, it might be an and, but that end will need to be + ;; reverse tracked to see if it belongs to valid argument block. + (and (string= (nth 1 parent) "end") + (save-excursion + (goto-char (nth 2 parent)) + (matlab--scan-block-backward) + (let ((prevblock (matlab--mk-keyword-node))) + (string= (nth 1 prevblock) "arguments"))) + ) + )) + )))) + +(defun matlab--scan-derive-block-state (providedstate filter) + "Return a block state for current point. +If PROVIDEDSTATE is non nil, use that. +Return nil if no valid block under pt." + (or providedstate + (let ((thiskeyword (matlab--mk-keyword-node))) + (if (or (not thiskeyword) + (not (matlab--valid-keyword-point)) + (not (memq (car thiskeyword) filter)) + ) + nil + (push thiskeyword providedstate))))) + +;;; Keyword Scanning +;; +;; Use for font locking +(defun matlab--scan-next-keyword (keyword-type limit) + "Scan for the next keyword of KEYWORD-TYPE and stop. +Sets match data to be around the keyword. +If nothing is found before LIMIT, then stop and return nil." + (let* ((regex (matlab-keyword-regex keyword-type)) + (filter (cond ((eq keyword-type 'mcos) + #'matlab--valid-mcos-keyword-point) + ((eq keyword-type 'args) + #'matlab--valid-arguments-keyword-point) + (t + ;; Not sure - do the super check + #'matlab--valid-keyword-node)))) + (matlab-re-search-keyword-forward regex limit t filter))) + +;;; Block Scanning +;; +(defun matlab--scan-block-forward (&optional bounds state) + "Scan forward over 1 MATLAB block construct. +Return current state on exit. + nil - success + non-nil - indicates incomplete scanning +Also skips over all nexted block constructs along the way. +Assumes cursor is in a valid starting state, otherwise ERROR. +If cursor is on a middle-block construct like else, case, ERROR. + +Optional BOUNDS is a point in the buffer past which we won't scan. +Optional STATE is the current parsing state to start from. +Use STATE to stop/start block scanning partway through." + (let ((blockstate (matlab--scan-derive-block-state state '(decl args mcos ctrl))) + (thiskeyword nil) + (stop nil) + (regex (matlab-keyword-regex 'blocks)) + ) + + (when (not blockstate) (error "Not on valid block start.")) + + (when (not state) (skip-syntax-forward "w")) ;; skip keyword + + (while (and blockstate (not stop)) + (if (not (setq thiskeyword (matlab-re-search-keyword-forward regex bounds t))) + (progn + (setq stop t) + (when (and (not matlab-functions-have-end) + (eq (car (car blockstate)) 'decl)) + (goto-char (point-max)) + (pop blockstate))) + (cond ((eq (car thiskeyword) 'end) + ;; On end, pop last start we pushed + (pop blockstate)) + ((eq (car thiskeyword) 'mcos) + (if (matlab--valid-mcos-keyword-point (car blockstate)) + (push thiskeyword blockstate) + ;; else, just skip it + )) + ((eq (car thiskeyword) 'args) + (if (matlab--valid-arguments-keyword-point (car blockstate)) + (push thiskeyword blockstate) + ;; else, just skip it, not a keyword + )) + ((and (not matlab-functions-have-end) + (eq (car thiskeyword) 'decl) + (eq (car (car blockstate)) 'decl) + ) + ;; No ends on functions - in this case we need treat a function as an end. + ;; b/c you can't have nested functions, but only if the thing we try to match + ;; it to is another fcn. + ;; This POP should result in empty state. + (pop blockstate) + (goto-char (match-beginning 1))) + (t + (push thiskeyword blockstate))) + )) + blockstate)) + +(defun matlab--scan-block-forward-up (&optional bounds) + "Like `matlab--scan-block-forward', but cursor is not on a keyword. +Instead, travel to end as if on keyword." + (let ((currentstate '((unknown "" 0)))) + (matlab--scan-block-forward bounds currentstate))) + + +(defun matlab--scan-block-backward (&optional bounds state) + "Scan forward over 1 MATLAB block construct. +Return current state on exit. + nil - success + non-nil - indicates incomplete scanning +Also skips over all nexted block constructs along the way. +Assumes cursor is in a valid starting state, otherwise ERROR. +If cursor is on a middle-block construct like else, case, ERROR. + +Optional BOUNDS is a point in the buffer past which we won't scan. +Optional STATE is the current parsing state to start from. +Use STATE to stop/start block scanning partway through." + (let ((blockstate (matlab--scan-derive-block-state state '(end))) + (thiskeyword nil) + (stop nil) + (regex (matlab-keyword-regex 'blocks)) + ) + + (when (not blockstate) (error "Not on valid block end.")) + + (when (not state) (skip-syntax-backward "w")) ;; skip keyword + + (while (and blockstate (not stop)) + (if (not (setq thiskeyword (matlab-re-search-keyword-backward regex bounds t))) + (setq stop t) + (cond ((eq (car thiskeyword) 'end) + ;; On end, push this keyword + (push thiskeyword blockstate)) + ((eq (car thiskeyword) 'mcos) + (when (matlab--valid-mcos-keyword-point nil) + (pop blockstate) + )) + ((eq (car thiskeyword) 'args) + (when (matlab--valid-arguments-keyword-point nil) + (pop blockstate) + )) + (t + (pop blockstate))) + )) + blockstate)) + +(defun matlab--scan-block-backward-up (&optional bounds) + "Like `matlab--scan-block-forward', but cursor is not on a keyword. +Instead, travel to end as if on keyword." + (let ((currentstate '((end "end" 0)))) + (matlab--scan-block-backward bounds currentstate))) + +(defun matlab--scan-block-backward-up-until (types &optional bounds) + "Call `matlab--scan-block-backward-up' until we find a keyword of TYPES. +Return a keyword node when a matching node is found. +Limit search to within BOUNDS. If keyword not found, return nil." + (when (symbolp types) (setq types (list types))) + (let ((node nil) (done nil) (start (point))) + (while (and (not done) + (or (not node) (not (memq (car node) types)))) + (if (matlab--scan-block-backward-up bounds) + (setq done t) + (setq node (matlab--mk-keyword-node)))) + (if (not done) + node + (goto-char start) + nil))) + +;;; Searching for keywords +;; +;; These utilities will simplify searching for code bits by skipping +;; anything in a comment or string. +(defun matlab-re-search-keyword-forward (regexp &optional bound noerror bonustest) + "Like `re-search-forward' but will not match content in strings or comments. +If BONUSTEST is a function, use it to test each match if it is valid. If not +then skip and keep searching." + (let ((ans nil) (case-fold-search nil) (err nil)) + (save-excursion + (while (and (not ans) (not err) + (or (not bound) (< (point) bound)) + (setq ans (re-search-forward regexp bound noerror))) + ;; Check for simple cases that are invalid for keywords + ;; for strings, comments, and lists, skip to the end of them + ;; to not waste time searching for keywords inside. + (cond ((matlab-end-of-string-or-comment) + ;; Test is only IF in a comment. Also skip other comments + ;; once we know we were in a comment. + (matlab-end-of-string-or-comment t) + (setq ans nil)) + ((matlab-in-list-p) + (condition-case nil + ;; Protect against unterminated lists. + (matlab-end-of-outer-list) + ;; if no longer in a list, say we're done, move to end + ;; of the buffer. + (error (goto-char (point-max)) + (setq err t))) + (setq ans nil)) + ((matlab-syntax-keyword-as-variable-p) + (setq ans nil)) + ((and bonustest (save-match-data (not (funcall bonustest)))) + (setq ans nil)) + ))) + (when ans (goto-char ans) (matlab--mk-keyword-node)))) + +(defun matlab-re-search-keyword-backward (regexp &optional bound noerror) + "Like `re-search-backward' but will not match content in strings or comments." + (let ((ans nil) (case-fold-search nil)) + (save-excursion + (while (and (not ans) + (or (not bound) (> (point) bound)) + (setq ans (re-search-backward regexp bound noerror))) + ;; Check for simple cases that are invalid for keywords + ;; for strings, comments, and lists, skip to the end of them + ;; to not waste time searching for keywords inside. + (cond ((matlab-beginning-of-string-or-comment) + (matlab-beginning-of-string-or-comment t) + (setq ans nil)) + ((matlab-beginning-of-outer-list) + (setq ans nil)) + ((matlab-syntax-keyword-as-variable-p) + (setq ans nil)) + ))) + (when ans (goto-char ans) (matlab--mk-keyword-node)))) + +;;; Quick Queries +;; +(defun matlab-scan-block-start-context () + "Return a context for the block start matching block point is in. +assumes pt is NOT on an end. List contains: + 0 - type of keyword the end matched. + 1 - column the keyword is on. + 2 - lvl1 context of line the keyword is on + 3 - lvl1 context of line at beginning of cmnd found keyword is in. + +Items 2 and 3 are likely the same but could be different." + (save-excursion + (matlab--scan-block-backward-up) + (let* ((keyword (matlab-on-keyword-p)) + (column (current-column)) + (lvl1-match (matlab-compute-line-context 1)) + (lvl1-bgn (save-excursion (matlab-scan-beginning-of-command lvl1-match) + (matlab-compute-line-context 1)))) + (list keyword column lvl1-match lvl1-bgn)))) + + +;;; Caching +;; +(defvar matlab-scan-temporal-cache nil + "Cache of recently computed line contexts. +Used to speed up repeated queries on the same set of lines.") +(make-variable-buffer-local 'matlab-scan-temporal-cache) +(defvar matlab-scan-cache-max 10 + "Largest size of the cache. +Larger means less computation, but more time scanning. +Since the list isn't sorted, not optimizations possible.") + +(defun matlab-scan-cache-get () + "Get a cached context." + (let ((pt (point)) + (cache matlab-scan-temporal-cache)) + (while (and cache (/= pt (nth mlf-point (car cache)))) + (setq cache (cdr cache))) + ;; If we found a match, return it. + (if (and cache (= pt (nth mlf-point (car cache)))) + (progn + (matlab-scan-stat-inc 'lvl1) + (car cache)) + (matlab-scan-stat-inc 'lvl1-miss) + nil))) + +(defun matlab-scan-cache-put (ctxt) + "Put a context onto the cache. +Make sure the cache doesn't exceed max size." + (push ctxt matlab-scan-temporal-cache) + (setcdr (or (nthcdr matlab-scan-cache-max matlab-scan-temporal-cache) + (cons nil nil)) + nil)) + + +(defun matlab-scan-before-change-fcn (start end &optional length) + "Function run in after change hooks." + ;;(setq matlab-scan-temporal-cache nil)) + (let ((pt (point)) + (cache matlab-scan-temporal-cache) + (newcache nil)) + ;; Flush whole lines. + (save-excursion + (goto-char start) + (setq start (point-at-bol))) + ;; Only drop items AFTER the start of our region. + (while cache + (if (<= start (matlab-line-point (car cache))) + (matlab-scan-stat-inc 'flushskip) + (push (car cache) newcache) + (matlab-scan-stat-inc 'flush)) + (setq cache (cdr cache))) + ;;(setq newcache nil) + ;;(when (and newcache (symbolp (car newcache))) (debug)) + (setq matlab-scan-temporal-cache newcache) + )) + +(defun matlab-scan-setup () + "Setup use of the indent cache for the current buffer." + (interactive) + (add-hook 'before-change-functions 'matlab-scan-before-change-fcn t) + (setq matlab-scan-temporal-cache nil)) + +(defun matlab-scan-disable () + "Setup use of the indent cache for the current buffer." + (interactive) + (remove-hook 'before-change-functions 'matlab-scan-before-change-fcn t) + (setq matlab-scan-temporal-cache nil)) + + +;;; Debugging and Querying +;; +(defvar matlab-scan-cache-stats nil + "Cache stats for tracking effectiveness of the cache.") +(defun matlab-scan-stat-reset (&optional arg) + "Reset the stats cache. +With no arg, disable gathering stats. +With arg, enable gathering stats, and flush old stats." + (interactive "P") + (if arg + (progn (setq matlab-scan-cache-stats nil) + (message "Disable matlab scanner stats gathering.")) + (message "Emable matlab scanner stats gathering.") + (setq matlab-scan-cache-stats (matlab-obarray-make 13)))) + +(defun matlab-scan-stat-inc (thing) + "Increment the stat associated with thing." + (when matlab-scan-cache-stats + (let ((sym (intern-soft (symbol-name thing) matlab-scan-cache-stats))) + (when (not sym) + (set (setq sym (intern (symbol-name thing) matlab-scan-cache-stats)) 0)) + (set sym (1+ (symbol-value sym)))) + ;;(matlab-scan-stats-print 'summary) + ) + ) + +(defun matlab-scan-stats-print (&optional summary) + "Display stats for scanner hits." + (interactive "P") + (let ((res nil)) + (mapatoms (lambda (sym) + (push (cons (symbol-name sym) (symbol-value sym)) res)) + matlab-scan-cache-stats) + (setq res (sort res (lambda (a b) (string< (car a) (car b))))) + (if summary + (when (not noninteractive) + ;; show a short form. + (message (mapconcat (lambda (pair) + (concat (car pair) ":" (format "%d" (cdr pair)))) + res " "))) + ;; Else, show a long form. + (let ((printfcn (lambda (pair) + (princ (concat (format "%-8s" (concat (car pair) ":")) "\t" + (format "%d" (cdr pair)) "\n"))))) + (if noninteractive + (progn + ;; Print to stdout when in batch mode. + (princ "\nCache Key\tHits\n") + (mapc printfcn res) + (princ "---\n")) + ;; Display in a buffer + (with-output-to-temp-buffer "*MATLAB SCANNER STATS*" + (princ "Cache Key\tHits\n----------\t------\n") + (mapc printfcn res))) + )))) + +(defun matlab-describe-line-indent-context (&optional lvl1 nodisp) + "Describe the indentation context for the current line. +If optional LVL1 is specified, describe that instead of computing. +If optional NODISP, then don't display, just return the msg." + (interactive) + (back-to-indentation) + (let* ((MSG1 "") + (lvl1 (or lvl1 (matlab-compute-line-context 1))) + ;(lvl2 (matlab-compute-line-context 2)) + ) + (let* ((paren-inner-char (nth mlf-paren-inner-char lvl1)) + (open (format "%c" (or paren-inner-char ?\())) + (close (format "%c" + (cond ((not paren-inner-char) ?\)) + ((= paren-inner-char ?\() ?\)) + ((= paren-inner-char ?\[) ?\]) + ((= paren-inner-char ?\{) ?\}) + (t ??)))) + (innerparenstr (format "%s%d%s" open (nth mlf-paren-depth lvl1) close)) + (outerp-char (nth mlf-paren-outer-char lvl1)) + (outerp-open (if outerp-char (format "%c" outerp-char) "")) + (outerp-close (if (not outerp-char) "" + (format "%c" + (cond ((= outerp-char ?\() ?\)) + ((= outerp-char ?\[) ?\]) + ((= outerp-char ?\{) ?\}) + (t ??))))) + (outerparenopen "") + (outerparenclose "") + (extraopen "") + (extraclose "") + ) + (cond ((= (nth mlf-paren-depth lvl1) 0) + ;; 0 means no parens - so shade out parens to indicate. + (setq open (propertize open 'face 'shadow) + close (propertize close 'face 'shadow))) + ((<= (nth mlf-paren-depth lvl1) 1) + ;; If 1 or fewer parens, clear out outer chars + (setq outerp-open "" + outerp-close "")) + ((> (nth mlf-paren-depth lvl1) 2) + ;; If more than 2, signal more unknown parens in between + (setq outerp-open (concat outerp-open (string (decode-char 'ucs #x2026))) + outerp-close (concat (string (decode-char 'ucs #x2026)) outerp-close)))) + (if (< (nth mlf-paren-delta lvl1) 0) + (setq extraopen (format "<%d" (abs (nth mlf-paren-delta lvl1)))) + (when (> (nth mlf-paren-delta lvl1) 0) + (setq extraclose (format "%d>" (nth mlf-paren-delta lvl1))))) + + + (setq MSG1 + (format "%s%s >>%d %s%s%s%s%s %s %s" (nth mlf-ltype lvl1) + (format " %s" (or (nth mlf-stype lvl1) "")) + (nth mlf-indent lvl1) + ;; paren system + extraopen + outerp-open + innerparenstr + outerp-close + extraclose + (cond ((eq (nth mlf-end-comment-type lvl1) 'comment) "%") + ((eq (nth mlf-end-comment-type lvl1) 'ellipsis) "...") + ((eq (nth mlf-end-comment-type lvl1) 'commanddual) "-command dual") + (t "")) + (if (matlab-line-end-comment-column lvl1) + (format " %d" (matlab-line-end-comment-column lvl1)) + "") + )) + ) +;; (let* ((lvl2 (matlab-compute-line-context-lvl-2 lvl1)) +;; ;;(comment +;; ) + + (if nodisp + MSG1 + (message "%s" MSG1)) + )) + + +(provide 'matlab-scan) + +;;; matlab-indent.el ends here diff --git a/matlab-shell-gud.el b/matlab-shell-gud.el index 68ab926..a532220 100644 --- a/matlab-shell-gud.el +++ b/matlab-shell-gud.el @@ -89,7 +89,6 @@ Disable this option if the tooltips are too slow in your setup." ;; (gud-def gud-print "% gud-print not available" "\C-p" "gud-print not available.") (when window-system - (matlab-frame-init) (setq gud-matlab-tool-bar-map (let ((map (make-sparse-keymap))) @@ -373,10 +372,10 @@ LONGESTNAME specifies the how long the longest name we can expect is." "Specify a NEWSTACK provided by MATLAB to replace the old one." (setq mlg-stack nil) (dolist (L newstack) - (push (mlg-stack-frame "" - :file (nth 0 L) - :name (nth 1 L) - :line (nth 2 L)) + (push (make-instance 'mlg-stack-frame + :file (nth 0 L) + :name (nth 1 L) + :line (nth 2 L)) mlg-stack)) (setq mlg-stack (nreverse mlg-stack)) (mlg-refresh-stack-buffer) @@ -598,9 +597,10 @@ LONGESTNAME specifies the how long the longest name we can expect is." (setq found t))) (when (not found) (setq matlab-gud-visible-breakpoints - (cons (mlg-breakpoint "" :file file - :name fcn - :line line) + (cons (make-instance 'mlg-breakpoint + :file file + :name fcn + :line line) matlab-gud-visible-breakpoints)) (mlg-activate (car matlab-gud-visible-breakpoints)) )) diff --git a/matlab-shell.el b/matlab-shell.el index b2943d8..cb64bfe 100644 --- a/matlab-shell.el +++ b/matlab-shell.el @@ -204,10 +204,27 @@ If multiple prompts are seen together, only call this once.") ;;; Font Lock ;; ;; Extra font lock keywords for the MATLAB shell. -(defvar matlab-shell-font-lock-keywords +(defconst matlab-shell-font-lock-keywords + (list + ;; How about Errors? + '("^\\(Error in\\|Syntax error in\\)\\s-+==>\\s-+\\(.+\\)$" + (1 font-lock-comment-face) (2 font-lock-string-face)) + ;; and line numbers + '("^\\(\\(On \\)?line [0-9]+\\)" 1 font-lock-comment-face) + ;; User beep things + '("\\(\\?\\?\\?[^\n]+\\)" 1 font-lock-comment-face) + ) + "Additional keywords used by MATLAB when reporting errors in interactive\ +mode.") + +(defconst matlab-shell-font-lock-keywords-1 + (append matlab-basic-font-lock-keywords + matlab-shell-font-lock-keywords) + "Keyword symbol used for basic font-lock for MATLAB shell.") + +(defconst matlab-shell-object-output-font-lock-keywords (list ;; Startup notices - ;; Various notices '(" M A T L A B " 0 'underline) '("All Rights Reserved" 0 'italic) '("\\(\\(?:(c)\\)?\\s-+Copyright[^\n]+\\)" 1 font-lock-comment-face) @@ -218,33 +235,38 @@ If multiple prompts are seen together, only call this once.") '("^To get started, type doc.$" 0 font-lock-comment-face prepend) '("For product information, [^\n]+" 0 font-lock-comment-face) - ;; How about Errors? - '("^\\(Error in\\|Syntax error in\\)\\s-+==>\\s-+\\(.+\\)$" - (1 font-lock-comment-face) (2 font-lock-string-face)) - ;; and line numbers - '("^\\(\\(On \\)?line [0-9]+\\)" 1 font-lock-comment-face) - ;; User beep things - '("\\(\\?\\?\\?[^\n]+\\)" 1 font-lock-comment-face) ;; Useful user commands, but not useful programming constructs '("\\<\\(demo\\|whatsnew\\|info\\|subscribe\\|help\\|doc\\|lookfor\\|what\ \\|whos?\\|cd\\|clear\\|load\\|save\\|helpdesk\\|helpwin\\)\\>" 1 font-lock-keyword-face) + ;; disp of objects usually looks like this: + '("^\\s-*\\(\\w+\\) with properties:" (1 font-lock-type-face)) + ;; object output - highlight property names after 'with properties:' indicator + ;; NOTE: Normally a block like this would require us to use `font-lock-multiline' feature + ;; but since this is shell output, and not a thing you edit, we can skip it and rely + ;; on matlab-shell dumping the text as a unit. + '("^\\s-*\\(\\w+ with properties:\\)\n\\s-*\n" + ("^\\s-*\\(\\w+\\):[^\n]+$" ;; match the property before the : + ;; Extend search region across lines. + (save-excursion (re-search-forward "\n\\s-*\n" nil t) + (beginning-of-line) + (point)) + nil + (1 font-lock-variable-name-face))) + '("[[{]\\([0-9]+\\(?:x[0-9]+\\)+ \\w+\\)[]}]" (1 font-lock-comment-face)) ) - "Additional keywords used by MATLAB when reporting errors in interactive\ -mode.") + "Highlight various extra outputs that are typical for MATLAB.") -(defvar matlab-shell-font-lock-keywords-1 - (append matlab-font-lock-keywords matlab-shell-font-lock-keywords) - "Keyword symbol used for font-lock mode.") +(defconst matlab-shell-font-lock-keywords-2 + (append matlab-shell-font-lock-keywords-1 + matlab-function-font-lock-keywords + matlab-shell-object-output-font-lock-keywords) + "Keyword symbol used for gaudy font-lock for MATLAB shell.") -(defvar matlab-shell-font-lock-keywords-2 - (append matlab-shell-font-lock-keywords-1 matlab-gaudy-font-lock-keywords) - "Keyword symbol used for gaudy font-lock symbols.") - -(defvar matlab-shell-font-lock-keywords-3 +(defconst matlab-shell-font-lock-keywords-3 (append matlab-shell-font-lock-keywords-2 matlab-really-gaudy-font-lock-keywords) - "Keyword symbol used for really gaudy font-lock symbols.") + "Keyword symbol used for really gaudy font-lock for MATLAB shell.") ;;; ROOT ;; @@ -1575,16 +1597,9 @@ Snatched and hacked from dired-x.el" (search-forward-regexp comint-prompt-regexp) (buffer-substring (point) (matlab-point-at-eol))))) (save-excursion - ;; In matlab buffer, find all the text for a command. - ;; so back over until there is no more continuation. - (while (save-excursion (forward-line -1) (matlab-lattr-cont)) - (forward-line -1)) - ;; Go forward till there is no continuation - (beginning-of-line) - (let ((start (point))) - (while (matlab-lattr-cont) (forward-line 1)) - (end-of-line) - (buffer-substring start (point)))))) + (buffer-substring-no-properties + (matlab-scan-beginning-of-command) + (matlab-scan-end-of-command))))) (defun matlab-non-empty-lines-in-string (str) "Return number of non-empty lines in STR." @@ -2164,7 +2179,13 @@ Similar to `comint-send-input'." )) ;; If not changing dir, maybe we need to use 'run' command instead? - (let ((cmd (concat "run('" dir fn-name "')"))) + (let* ((match 0) + (tmp (while (setq match (string-match "'" param match)) + (setq param (replace-match "''" t t param)) + (setq match (+ 2 match)))) + (cmd (concat "emacsrun('" dir fn-name "'" + (if (string= param "") "" (concat ", '" param "'")) + ")"))) (matlab-shell-send-command cmd))) )))) @@ -2175,20 +2196,22 @@ Similar to `comint-send-input'." (defun matlab-shell-run-cell () "Run the cell the cursor is in." (interactive) - (let ((start (save-excursion (forward-page -1) - (if (looking-at "function") - (error "You are not in a cell. Try `matlab-shell-save-and-go' instead")) - (when (matlab-ltype-comm) - ;; Skip over starting comment from the current cell. - (matlab-end-of-command 1) - (end-of-line) - (forward-char 1)) - (point))) - (end (save-excursion (forward-page 1) - (when (matlab-ltype-comm) - (beginning-of-line) - (forward-char -1)) - (point)))) + (let ((start (save-excursion + (forward-page -1) + (if (looking-at "function") + (error "You are not in a cell. Try `matlab-shell-save-and-go' instead")) + (when (matlab-line-comment-p (matlab-compute-line-context 1)) + ;; Skip over starting comment from the current cell. + (matlab-end-of-command) + (end-of-line) + (forward-char 1)) + (point))) + (end (save-excursion + (forward-page 1) + (when (matlab-line-comment-p (matlab-compute-line-context 1)) + (beginning-of-line) + (forward-char -1)) + (point)))) (matlab-shell-run-region start end t))) (defun matlab-shell-run-region-or-line () diff --git a/matlab-syntax.el b/matlab-syntax.el new file mode 100644 index 0000000..ff6a897 --- /dev/null +++ b/matlab-syntax.el @@ -0,0 +1,573 @@ +;;; matlab-syntax.el --- Manage MATLAB syntax tables and buffer parsing. +;; +;; Copyright (C) 2021 Eric Ludlam +;; +;; Author: +;; +;; 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 of the +;; License, 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 this program. If not, see https://www.gnu.org/licenses/. + +;;; Commentary: +;; +;; Manage syntax handling for `matlab-mode'. +;; Matlab's syntax for comments and strings can't be handled by a standard +;; Emacs syntax table. This code handles the syntax table, and special +;; scanning needed to augment a buffer's syntax for all our special cases. +;; +;; This file also handles all the special parsing needed to support indentation, +;; block scanning, and the line. + +(require 'matlab-compat) + +;;; Code: +(defvar matlab-syntax-support-command-dual t + "Non-nil means to support command dual for indenting and syntax highlight. +Does not work well in classes with properties with datatypes.") +(make-variable-buffer-local 'matlab-syntax-support-command-dual) +(put 'matlab-syntax-support-command-dual 'safe-local-variable #'booleanp) + + +(defvar matlab-syntax-table + (let ((st (make-syntax-table (standard-syntax-table)))) + ;; Comment Handling: + ;; Multiline comments: %{ text %} + ;; Single line comments: % text (single char start) + ;; Ellipsis omments: ... text (comment char is 1st char after 3rd dot) + ;; ^ handled in `matlab--syntax-propertize' + (modify-syntax-entry ?% "< 13" st) + (modify-syntax-entry ?{ "(} 2c" st) + (modify-syntax-entry ?} "){ 4c" st) + (modify-syntax-entry ?\n ">" st) + + ;; String Handling: + ;; Character vector: 'text' + ;; String: "text" + ;; These next syntaxes are handled with `matlab--syntax-propertize' + ;; Transpose: varname' + ;; Quoted quotes: ' don''t ' or " this "" " + ;; Unterminated Char V: ' text + (modify-syntax-entry ?' "\"" st) + (modify-syntax-entry ?\" "\"" st) + + ;; Words and Symbols: + (modify-syntax-entry ?_ "_" st) + + ;; Punctuation: + (modify-syntax-entry ?\\ "." st) + (modify-syntax-entry ?\t " " st) + (modify-syntax-entry ?+ "." st) + (modify-syntax-entry ?- "." st) + (modify-syntax-entry ?* "." st) + (modify-syntax-entry ?/ "." st) + (modify-syntax-entry ?= "." st) + (modify-syntax-entry ?< "." st) + (modify-syntax-entry ?> "." st) + (modify-syntax-entry ?& "." st) + (modify-syntax-entry ?| "." st) + + ;; Parentheticl blocks: + ;; Note: these are in standard syntax table, repeated here for completeness. + (modify-syntax-entry ?\( "()" st) + (modify-syntax-entry ?\) ")(" st) + (modify-syntax-entry ?\[ "(]" st) + (modify-syntax-entry ?\] ")[" st) + ;;(modify-syntax-entry ?{ "(}" st) - Handled as part of comments + ;;(modify-syntax-entry ?} "){" st) + + st) + "MATLAB syntax table") + +(defvar matlab-navigation-syntax-table + (let ((st (copy-syntax-table matlab-syntax-table))) + ;; Make _ a part of words so we can skip them better + (modify-syntax-entry ?_ "w" st) + st) + "The syntax table used when navigating blocks.") + +(defmacro matlab-navigation-syntax (&rest forms) + "Set the current environment for syntax-navigation and execute FORMS." + (declare (indent 0)) + (list 'let '((oldsyntax (syntax-table)) + (case-fold-search nil)) + (list 'unwind-protect + (list 'progn + '(set-syntax-table matlab-navigation-syntax-table) + (cons 'progn forms)) + '(set-syntax-table oldsyntax)))) + +(add-hook 'edebug-setup-hook + (lambda () + (def-edebug-spec matlab-navigation-syntax def-body))) + +;;; Buffer Scanning for Syntax Table Augmentation +;; +;; To support all our special syntaxes via syntax-ppss (parse partial +;; sexp), we need to scan the buffer for patterns, and then leave +;; behind the hints pps needs to do the right thing. +;; +;; Support is broken up in these functions: +;; * matlab--put-char-category - Apply a syntax category to a character +;; * matlab--syntax-symbol - Create a syntax category symbol +;; * matlab--syntax-propertize - Used as `syntax-propertize-function' for +;; doing the buffer scan to augment syntxes. +;; * matlab--scan-line-* - Scan for specific types of syntax occurances. + +(defun matlab--put-char-category (pos category) + "At character POS, put text CATEGORY." + (when (not (eobp)) + (put-text-property pos (1+ pos) 'category category) + (put-text-property pos (1+ pos) 'mcm t)) + ) + +(defmacro matlab--syntax-symbol (symbol syntax doc) + "Create a new SYMBOL used as a text property category with SYNTAX." + (declare (indent defun)) + `(progn (defvar ,symbol ,syntax ,doc) + (set ',symbol ,syntax) ;; So you can re-eval it. + (put ',symbol 'syntax-table ,symbol) + )) + +(matlab--syntax-symbol matlab--command-dual-syntax '(15 . nil) ;; Generic string + "Syntax placed on end-of-line for unterminated strings.") +(put 'matlab--command-dual-syntax 'command-dual t) ;; Font-lock cookie + +(matlab--syntax-symbol matlab--unterminated-string-syntax '(15 . nil) ;; Generic string end + "Syntax placed on end-of-line for unterminated strings.") +(put 'matlab--unterminated-string-syntax 'unterminated t) ;; Font-lock cookie + +(matlab--syntax-symbol matlab--ellipsis-syntax (string-to-syntax "< ") ;; comment char + "Syntax placed on ellipsis to treat them as comments.") + +(matlab--syntax-symbol matlab--not-block-comment-syntax (string-to-syntax "(}") ;; Just a regular open brace + "Syntax placed on ellipsis to treat them as comments.") + +(defun matlab--syntax-propertize (&optional start end) + "Scan region between START and END for unterminated strings. +Only scans whole-lines, as MATLAB is a line-based language. +If region is not specified, scan the whole buffer. +See `matlab--scan-line-for-ellipsis', `matlab--san-line-bad-blockcomment', +and `matlab--scan-line-for-unterminated-string' for specific details." + (save-match-data ;; avoid 'Syntax Checking transmuted the match-data' + (save-excursion + ;; Scan region, but always expand to beginning of line + (goto-char (or start (point-min))) + (beginning-of-line) + ;; Clear old properties + (remove-text-properties (point) (save-excursion (goto-char (or end (point-max))) + (end-of-line) (point)) + '(category nil mcm nil)) + ;; Apply properties + (while (and (not (>= (point) (or end (point-max)))) (not (eobp))) + + (when matlab-syntax-support-command-dual + ;; Commandl line dual comes first to prevent wasting time + ;; in later checks. + (beginning-of-line) + (when (matlab--scan-line-for-command-dual) + (matlab--put-char-category (point) 'matlab--command-dual-syntax) + (end-of-line) + (matlab--put-char-category (point) 'matlab--command-dual-syntax) + )) + + ;; Multiple ellipsis can be on a line. Find them all + (beginning-of-line) + (while (matlab--scan-line-for-ellipsis) + ;; Mark ellipsis as if a comment. + (matlab--put-char-category (point) 'matlab--ellipsis-syntax) + (forward-char 3) + ) + + ;; Multiple invalid block comment starts possible. Find them all + (beginning-of-line) + (while (matlab--scan-line-bad-blockcomment) + ;; Mark 2nd char as just open brace, not punctuation. + (matlab--put-char-category (point) 'matlab--not-block-comment-syntax) + ) + + ;; Look for an unterminated string. Only one possible per line. + (beginning-of-line) + (when (matlab--scan-line-for-unterminated-string) + ;; Mark this one char plus EOL as end of string. + (let ((start (point))) + (matlab--put-char-category (point) 'matlab--unterminated-string-syntax) + (end-of-line) + (matlab--put-char-category (point) 'matlab--unterminated-string-syntax) + )) + + (beginning-of-line) + (forward-line 1)) + ))) + +(defconst matlab-syntax-commanddual-functions + '("warning" "disp" "cd" + ;; debug + "dbstop" "dbclear" + ;; Graphics + "print" "xlim" "ylim" "zlim" "grid" "hold" "box" "colormap" "axis") + "Functions that are commonly used with commandline dual") +(defconst matlab-cds-regex (regexp-opt matlab-syntax-commanddual-functions 'symbols)) + +(defun matlab--scan-line-for-command-dual (&optional debug) + "Scan this line for command line duality strings." + ;; Note - add \s$ b/c we'll add that syntax to the first letter, and it + ;; might still be there during an edit! + (let ((case-fold-search nil)) + (when (and (not (nth 9 (syntax-ppss (point)))) + (looking-at + (concat "^\\s-*" + matlab-cds-regex + "\\s-+\\(\\s$\\|\\w\\|\\s_\\)"))) + (goto-char (match-beginning 2))))) + +(matlab--syntax-symbol matlab--transpose-syntax '(3 . nil) ;; 3 = symbol + "Treat ' as non-string when used as transpose.") + +(matlab--syntax-symbol matlab--quoted-string-syntax '(9 . nil) ;; 9 = escape in a string + "Treat '' or \"\" as not string delimeteres when inside a string.") + +(defun matlab--scan-line-for-unterminated-string (&optional debug) + "Scan this line for an unterminated string, leave cursor on starting string char." + ;; First, scan over all the string chars. + (save-restriction + (narrow-to-region (point-at-bol) (point-at-eol)) + (beginning-of-line) + (condition-case err + (while (re-search-forward "\\s\"\\|\\s<" nil t) + (let ((start-str (match-string 0)) + (start-char (match-beginning 0))) + (forward-char -1) + (if (looking-at "\\s<") + (progn + (matlab--scan-line-comment-disable-strings) + (forward-comment 1)) + ;; Else, check for valid string + (if (or (bolp) + (string= start-str "\"") + (save-excursion + (forward-char -1) + (not (looking-at "\\(\\w\\|\\s_\\|\\s)\\|\"\\|\\.\\)")))) + (progn + ;; Valid string start, try to skip the string + (forward-sexp 1) + ;; If we just finished and we have a double of ourselves, + ;; convert those doubles into punctuation. + (when (looking-at start-str) + (matlab--put-char-category (1- (point)) 'matlab--quoted-string-syntax) + ;; and try again. + (goto-char start-char) + )) + (when (string= start-str "'") + ;; If it isn't valid string, it's just transpose or something. + ;; convert to a symbol - as a VAR'', the second ' needs to think it + ;; is not after punctuation. + (matlab--put-char-category (point) 'matlab--transpose-syntax)) + ;; Move forward 1. + (forward-char 1) + ))) + nil) + (error + t)))) + +(defun matlab--scan-line-comment-disable-strings () + "Disable bad string chars syntax from point to eol. +Called when comments found in `matlab--scan-line-for-unterminated-string'." + (save-excursion + (while (re-search-forward "\\s\"" nil t) + (save-excursion + (matlab--put-char-category (1- (point)) 'matlab--transpose-syntax) + )))) + +(defun matlab--scan-line-bad-blockcomment () + "Scan this line for invalid block comment starts." + (when (and (re-search-forward "%{" (point-at-eol) t) (not (looking-at "\\s-*$"))) + (goto-char (1- (match-end 0))) + t)) + +(defun matlab--scan-line-for-ellipsis () + "Scan this line for an ellipsis." + (when (re-search-forward "\\.\\.\\." (point-at-eol) t) + (goto-char (match-beginning 0)) + t)) + +;;; Font Lock Support: +;; +;; The syntax specific font-lock support handles comments and strings. +;; +;; We'd like to support multiple kinds of strings and comments. To do +;; that we overload `font-lock-syntactic-face-function' with our own. +;; This does the same job as the orriginal, except we scan the start +;; for special cookies left behind by `matlab--syntax-propertize' and +;; use that to choose different fonts. +(defun matlab--font-lock-syntactic-face (pps) + "Return the face to use for the syntax specified in PPS." + ;; From the default in font-lock. + ;; (if (nth 3 state) font-lock-string-face font-lock-comment-face) + (if (nth 3 pps) + ;; This is a string. Check the start char to see if it was + ;; marked as an unterminate string. + (cond ((get-text-property (nth 8 pps) 'unterminated) + 'matlab-unterminated-string-face) + ((get-text-property (nth 8 pps) 'command-dual) + 'matlab-commanddual-string-face) + (t + 'font-lock-string-face)) + + ;; Not a string, must be a comment. Check to see if it is a + ;; cellbreak comment. + (cond ((and (< (nth 8 pps) (point-max)) + (= (char-after (1+ (nth 8 pps))) ?\%)) + 'matlab-cellbreak-face) + ((and (< (nth 8 pps) (point-max)) + (= (char-after (1+ (nth 8 pps))) ?\#)) + 'matlab-pragma-face) + ((and (< (nth 8 pps) (point-max)) + (looking-at "\\^\\| \\$\\$\\$")) + 'matlab-ignored-comment-face) + (t + 'font-lock-comment-face)) + )) + +;;; SETUP +;; +;; Connect our special logic into a running MATLAB Mode +;; replacing existing mechanics. +;; +;; Delete this if/when it becomes a permanent part of `matlab-mode'. + +(defun matlab-syntax-setup () + "Integrate our syntax handling into a running `matlab-mode' buffer. +Safe to use in `matlab-mode-hook'." + ;; Syntax Table support + (set-syntax-table matlab-syntax-table) + (make-local-variable 'syntax-propertize-function) + (setq syntax-propertize-function 'matlab--syntax-propertize) + ;; Comment handlers + (make-local-variable 'comment-start) + (make-local-variable 'comment-end) + (make-local-variable 'comment-start-skip) + (make-local-variable 'page-delimiter) + (setq comment-start "%" + comment-end "" + comment-start-skip "%\\s-+" + page-delimiter "^\\(\f\\|%%\\(\\s-\\|\n\\)\\)") + ;; Other special regexps handling different kinds of syntax. + (make-local-variable 'paragraph-start) + (setq paragraph-start (concat "^$\\|" page-delimiter)) + (make-local-variable 'paragraph-separate) + (setq paragraph-separate paragraph-start) + (make-local-variable 'paragraph-ignore-fill-prefix) + (setq paragraph-ignore-fill-prefix t) + + ;; Font lock + (make-local-variable 'font-lock-syntactic-face-function) + (setq font-lock-syntactic-face-function 'matlab--font-lock-syntactic-face) + ) + +;;; Syntax Testing for Strings and Comments +;; +;; These functions detect syntactic context based on the syntax table. +(defsubst matlab-cursor-in-string-or-comment () + "Return non-nil if the cursor is in a valid MATLAB comment or string." + (nth 8 (syntax-ppss (point)))) + +(defsubst matlab-cursor-in-comment () + "Return t if the cursor is in a valid MATLAB comment." + (nth 4 (syntax-ppss (point)))) + +(defsubst matlab-cursor-in-string (&optional incomplete) + "Return t if the cursor is in a valid MATLAB character vector or string scalar. +Note: INCOMPLETE is now obsolete +If the optional argument INCOMPLETE is non-nil, then return t if we +are in what could be a an incomplete string. (Note: this is also the default)" + (nth 3 (syntax-ppss (point)))) + +(defun matlab-cursor-comment-string-context (&optional bounds-sym) + "Return the comment/string context of cursor for the current line. +Return 'comment if in a comment. +Return 'string if in a string. +Return 'charvector if in a character vector +Return 'ellipsis if after an ... ellipsis +Return 'commanddual if in text interpreted as string for command dual +Return nil if none of the above. +Scans from the beginning of line to determine the context. +If optional BOUNDS-SYM is specified, set that symbol value to the +bounds of the string or comment the cursor is in" + (let* ((pps (syntax-ppss (point))) + (start (nth 8 pps)) + (end 0) + (syntax nil)) + ;; Else, inside something if 'start' is set. + (when start + (save-match-data + (save-excursion + (goto-char start) ;; Prep for extra checks. + (setq syntax + (cond ((eq (nth 3 pps) t) + (cond ((= (following-char) ?') + 'charvector) + ((= (following-char) ?\") + 'string) + (t + 'commanddual))) + ((eq (nth 3 pps) ?') + 'charvector) + ((eq (nth 3 pps) ?\") + 'string) + ((nth 4 pps) + (if (= (following-char) ?\%) + 'comment + 'ellipsis)) + (t nil))) + + ;; compute the bounds + (when (and syntax bounds-sym) + (if (memq syntax '(charvector string)) + ;;(forward-sexp 1) - overridden - need primitive version + (goto-char (scan-sexps (point) 1)) + (forward-comment 1) + (if (bolp) (forward-char -1))) + (set bounds-sym (list start (point)))) + ))) + + ;; Return the syntax + syntax)) + +(defsubst matlab-beginning-of-string-or-comment (&optional all-comments) + "If the cursor is in a string or comment, move to the beginning. +Returns non-nil if the cursor is in a comment." + (let* ((pps (syntax-ppss (point)))) + (prog1 + (when (nth 8 pps) + (goto-char (nth 8 pps)) + + t) + (when all-comments (forward-comment -100000))))) + +(defun matlab-end-of-string-or-comment (&optional all-comments) + "If the cursor is in a string or comment, move to the end. +If optional ALL-COMMENTS is non-nil, then also move over all +adjacent comments. +Returns non-nil if the cursor moved." + (let* ((pps (syntax-ppss (point))) + (start (point))) + (if (nth 8 pps) + (progn + ;; syntax-ppss doesn't have the end, so go to the front + ;; and then skip forward. + (goto-char (nth 8 pps)) + (if (nth 3 pps) + (goto-char (scan-sexps (point) 1)) + (forward-comment (if all-comments 100000 1))) + ;; If the buffer is malformed, we might end up before starting pt. + ;; so error. + (when (< (point) start) + (goto-char start) + (error "Error navitaging syntax.")) + t) + ;; else not in comment, but still skip 'all-comments' if requested. + (when (and all-comments (looking-at "\\s-*\\s<")) + (forward-comment 100000) + t) + ))) + +;;; Navigating Lists +;; +;; MATLAB's lists are (), {}, []. +;; We used to need to do special stuff, but now I think this +;; is just a call striaght to up-list. + +(defun matlab-up-list (count) + "Move forwards or backwards up a list by COUNT. +When travelling backward, use `syntax-ppss' counted paren +starts to navigate upward. +When travelling forward, use 'up-list' diretly, but disable +comment and string crossing." + (save-restriction + (matlab-beginning-of-string-or-comment) + (if (< count 0) + (let ((pps (syntax-ppss))) + (when (< (nth 0 pps) (abs count)) + (error "Cannot navigate up %d lists" (abs count))) + ;; When travelling in reverse, we can just use pps' + ;; parsed paren list in slot 9. + (let ((posn (reverse (nth 9 pps)))) ;; Location of parens + (goto-char (nth (1- (abs count)) posn)))) + ;; Else - travel forward + (up-list count nil t)) ;; will this correctly ignore comments, etc? + )) + +(defsubst matlab-in-list-p () + "If the cursor is in a list, return positions of the beginnings of the lists. +Returns nil if not in a list." + (nth 9 (syntax-ppss (point)))) + +(defsubst matlab-beginning-of-outer-list () + "If the cursor is in a list, move to the beginning of outermost list. +Returns non-nil if the cursor moved." + (let ((pps (syntax-ppss (point)))) + (when (nth 9 pps) (goto-char (car (nth 9 pps))) ))) + +(defun matlab-end-of-outer-list () + "If the cursor is in a list, move to the end of the outermost list.. +Returns non-nil if the cursor moved." + (let ((pps (syntax-ppss (point))) + (start (point))) + (when (nth 9 pps) + ;; syntax-ppss doesn't have the end, so go to the front + ;; and then skip forward. + (goto-char (car (nth 9 pps))) + (goto-char (scan-sexps (point) 1)) + ;; This checks for malformed buffer content + ;; that can cause this to go backwards. + (when (> start (point)) + (goto-char start) + (error "Malformed List")) + ))) + +;;; Useful checks for state around point. +;; +(defsubst matlab-syntax-keyword-as-variable-p () + "Return non-nil if the current word is treated like a variable. +This could mean it is: + * Field of a structure + * Assigned from or into with =" + (or (save-excursion (skip-syntax-backward "w") + (skip-syntax-backward " ") + (or (= (preceding-char) ?\.) + (= (preceding-char) ?=))) + (save-excursion (skip-syntax-forward "w") + (skip-syntax-forward " ") + (= (following-char) ?=)))) + +(defsubst matlab-valid-keyword-syntax () + "Return non-nil if cursor is not in a string, comment, or parens." + (let ((pps (syntax-ppss (point)))) + (not (or (nth 8 pps) (nth 9 pps))))) ;; 8 == string/comment, 9 == parens + +;;; Syntax Compat functions +;; +;; Left over old APIs. Delete these someday. +(defsubst matlab-move-simple-sexp-backward-internal (count) + "Move backward COUNT number of MATLAB sexps." + (let ((forward-sexp-function nil)) + (forward-sexp (- count)))) + +(defsubst matlab-move-simple-sexp-internal(count) + "Move over one MATLAB sexp COUNT times. +If COUNT is negative, travel backward." + (let ((forward-sexp-function nil)) + (forward-sexp count))) + +(provide 'matlab-syntax) + +;;; matlab-syntax.el ends here diff --git a/matlab.el b/matlab.el index 374e9a0..9eb7aa7 100644 --- a/matlab.el +++ b/matlab.el @@ -7,11 +7,11 @@ ;; Keywords: MATLAB(R) ;; Version: -(defconst matlab-mode-version "4.0" +(defconst matlab-mode-version "5.0" "Current version of MATLAB(R) mode.") ;; -;; Copyright (C) 1997-2020 Eric M. Ludlam +;; Copyright (C) 1997-2021 Eric M. Ludlam ;; Copyright (C) 1991-1997 Matthew R. Wette ;; ;; This program is free software; you can redistribute it and/or modify @@ -51,6 +51,8 @@ ;;; Code: (require 'matlab-compat) +(require 'matlab-syntax) +(require 'matlab-scan) (require 'easymenu) (require 'derived) @@ -85,11 +87,16 @@ nil (never) means that new *.m files will not enter :group 'matlab :type 'integer) -(defcustom matlab-cont-level 4 +(defcustom matlab-continuation-indent-level 4 "*Basic indentation after continuation if no other methods are found." :group 'matlab :type 'integer) +(defcustom matlab-array-continuation-indent-level 2 + "*Basic indentation after continuation within an array if no other methods are found." + :group 'matlab + :type 'integer) + (defcustom matlab-cont-requires-ellipsis t "*Specify if ellipses are required at the end of a line for continuation. Future versions of Matlab may not require ellipses ... , so a heuristic @@ -97,7 +104,7 @@ determining if there is to be continuation is used instead." :group 'matlab :type 'integer) -(defcustom matlab-case-level '(2 . 2) +(defcustom matlab-case-indent-level '(2 . 2) "*How far to indent case/otherwise statements in a switch. This can be an integer, which is the distance to indent the CASE and OTHERWISE commands, and how far to indent commands appearing in CASE @@ -114,103 +121,8 @@ should be ok." :group 'matlab :type 'sexp) -(defcustom matlab-align-to-paren t - "*Whether continuation lines should be aligned to the opening parenthesis. -When non-nil, continuation lines are aligned to the opening parenthesis if the -opening is not followed by only spaces and ellipses. When nil, continued lines -are simply indented by `matlab-cont-level'." - :group 'matlab - :type 'boolean - ) - -(defcustom matlab-indent-function-body 'MathWorks-Standard - "*If non-nil, indent body of function. -If the global value is nil, do not indent function bodies. -If the global value is t, always indent function bodies. -If the global value is 'guess, then the local value will be set to -either nil or t when the MATLAB mode is started in a buffer based on the -file's current indentation. -If the global value is 'MathWorks-Standard, then the local value is not -changed, and functions are indented based on `matlab-functions-have-end'." - :group 'matlab - :type '(choice (const :tag "Always" t) - (const :tag "Never" nil) - (const :tag "Guess" guess) - (const :tag "MathWorks Standard" - MathWorks-Standard)) - ) - -(make-variable-buffer-local 'matlab-indent-function-body) - -(defcustom matlab-functions-have-end t - "*If non-nil, functions-have-end minor mode is on by default. -If the value is 'guess, then we guess if a file has end when -`matlab-mode' is initialized." - :group 'matlab - :type 'boolean) - -(make-variable-buffer-local 'matlab-functions-have-end) - -(defun matlab-toggle-functions-have-end () - "Toggle `matlab-functions-have-end-minor-mode'." - (interactive) - (matlab-toggle-functions-have-end-minor-mode)) - -;; The following minor mode is on if and only if the above variable is true; -(easy-mmode-define-minor-mode matlab-functions-have-end-minor-mode - "Toggle functions-have-end minor mode, indicating function/end pairing." - nil - " function...end" - nil ; empty mode-map - ;; body of matlab-functions-have-end-minor-mode - (if matlab-functions-have-end-minor-mode - (setq matlab-functions-have-end t) - (setq matlab-functions-have-end nil) - ) - ) - -(defvar matlab-defun-regex) ;; Quiet compiler warning (is defined below) -(defun matlab-do-functions-have-end-p () - "Look at the contents of the current buffer and decide if functions have end. -If the current value of `matlab-functions-have-end' is 'guess, look @ the buffer. -If the value is t, then return that." - (if (eq matlab-functions-have-end 'guess) - (save-excursion - (goto-char (point-min)) - (if (re-search-forward matlab-defun-regex nil t) - (let ((matlab-functions-have-end t)) - (beginning-of-line) - (condition-case nil - (progn (matlab-forward-sexp) t) - (error nil)) - ) - nil - ) - ) - ;; Else, just return the default. - matlab-functions-have-end)) - -(defun matlab-toggle-functions-have-end-minor-mode () - "Toggle `matlab-functions-have-end-minor-mode' only for `matlab-mode' buffers." - (matlab-functions-have-end-minor-mode) - (if (and matlab-functions-have-end-minor-mode (not (eq major-mode 'matlab-mode))) - (progn - (matlab-functions-have-end-minor-mode -1) - (error "Mode `matlab-functions-have-end' minor mode is only for MATLAB Major mode"))) - (setq matlab-functions-have-end matlab-functions-have-end-minor-mode)) - -(defun matlab-indent-function-body-p () - "Non-nil if functions bodies are indented. -See `matlab-indent-function-body' variable." - (if (eq matlab-indent-function-body 'MathWorks-Standard) - ;; Dec '09 - ;; The MathWorks standard is the same as if functions have end. - matlab-functions-have-end - ;; Else, just return the variable. - matlab-indent-function-body)) - (defcustom matlab-indent-past-arg1-functions - "[sg]et\\(_param\\)?\\|waitfor\\|notify" + "\\_<\\([sg]et\\(_param\\)?\\|waitfor\\|notify\\)\\_>" "*Regex describing functions whose first arg is special. This specialness means that all following parameters which appear on continued lines should appear indented to line up with the second @@ -242,6 +154,216 @@ amount to use if MAXIMUM is reached." :type '(repeat (cons (character :tag "Open List Character") (sexp :tag "Number (max) or cons (max indent)")))) +(defcustom matlab-align-to-paren t + "*Whether continuation lines should be aligned to the opening parenthesis. +When non-nil, continuation lines are aligned to the opening parenthesis if the +opening is not followed by only spaces and ellipses. When nil, continued lines +are simply indented by `matlab-continuation-indent-level'." + :group 'matlab + :type 'boolean + ) + +(defcustom matlab-indent-function-body 'MathWorks-Standard + "*If non-nil, indent body of function. +If the global value is nil, do not indent function bodies. +If the global value is t, always indent function bodies. +If the global value is 'guess, then the local value will be set to +either nil or t when the MATLAB mode is started in a buffer based on the +file's current indentation. +If the global value is 'MathWorks-Standard, then the local value is not +changed, and functions are indented based on `matlab-functions-have-end'." + :group 'matlab + :type '(choice (const :tag "Always" t) + (const :tag "Never" nil) + (const :tag "Guess" guess) + (const :tag "MathWorks Standard" + MathWorks-Standard)) + ) +(make-variable-buffer-local 'matlab-indent-function-body) +(put 'matlab-indent-function-body 'safe-local-variable #'symbolp) + +(defcustom matlab-functions-have-end 'guess + "*If non-nil, functions-have-end minor mode is on by default. +If the value is 'guess, then we guess if a file has end when +`matlab-mode' is initialized." + :group 'matlab + :type 'boolean) + +(make-variable-buffer-local 'matlab-functions-have-end) +(put 'matlab-functions-have-end 'safe-local-variable #'symbolp) + +(defun matlab-toggle-functions-have-end () + "Toggle `matlab-functions-have-end-minor-mode'." + (interactive) + (matlab-toggle-functions-have-end-minor-mode)) + +;; The following minor mode is on if and only if the above variable is true; +(easy-mmode-define-minor-mode matlab-functions-have-end-minor-mode + "Toggle functions-have-end minor mode, indicating function/end pairing." + nil + (:eval (cond ((eq matlab-functions-have-end 'guess) + " function... ?") + ((eq matlab-functions-have-end 'class) + " classdef...end") + (matlab-functions-have-end + " function...end") + (t + " function..."))) + nil ; empty mode-map + ;; body of matlab-functions-have-end-minor-mode + (let ((type (matlab-guess-script-type))) + (if matlab-functions-have-end-minor-mode + (if (eq type 'empty) + (setq matlab-functions-have-end 'guess) + (setq matlab-functions-have-end type)) + (setq matlab-functions-have-end nil) + ) + ;; Depending on the kind of end, lets set other variables. + (cond ((eq matlab-functions-have-end 'guess) + ;;(setq matlab-syntax-support-command-dual t) + ) + ((eq matlab-functions-have-end 'class) + ;;(setq matlab-syntax-support-command-dual nil) + ) + (matlab-functions-have-end + ;;(setq matlab-syntax-support-command-dual t) + ) + (t + ;;(setq matlab-syntax-support-command-dual nil) + )) + )) + +(defvar matlab-last-script-type-guess nil + "The last time we guessed the script type, what was it?") +(defun matlab-last-guess-decl-p () + "Return non-nil if our last guess at a script type was function or class." + (memq matlab-last-script-type-guess '(function class))) + +(defun matlab-guess-script-type () + "Guess the type of script this `matlab-mode' file contains. +Returns one of 'empty, 'script, 'function, 'class." + (setq + matlab-last-script-type-guess + (save-excursion + (goto-char (point-min)) + (let ((lvl1 nil)) + (cond ((not (matlab-find-code-line)) + 'empty) + + ;; We found some code, what is it? + ((and (setq lvl1 (matlab-compute-line-context 1)) + (matlab-line-declaration-p lvl1)) + ;; We are on a decl - distinguis between type + (let ((str (matlab-line-first-word-text lvl1))) + (cond ((string= str "function") + 'function) + ((string= str "classdef") + 'class) + (t (error "Error in script guessing algorithm."))))) + + (t + ;; No function or class - just a script. + 'script)))))) + +(defun matlab-do-functions-have-end-p (&optional no-navigate) + "Look at the contents of the current buffer and decide if functions have end. +If the current value of `matlab-functions-have-end' is 'guess, look @ the buffer. +If the value is t, then return that." + (if (eq matlab-functions-have-end 'guess) + ;; Lets guess what we think the answer is. + (let ((type (matlab-guess-script-type))) + (cond ((eq type 'empty) + 'guess) ;; Keep guessing until we get some code. + ((eq type 'script) + 'script) ;; modern scripts can have functions, and they are required to have an end. + ((eq type 'class) + 'class) ;; classes always have ends. + (no-navigate + ;; Functions, but don't navigate ... stay in guess mode. + 'guess) + (t + ;; functions but do navigate - we need to see if there is an end. + (save-excursion + (goto-char (point-min)) + (matlab-find-code-line) + (let ((matlab-functions-have-end t)) ;; pretend we have ends + (back-to-indentation) + (if (eq (matlab-on-keyword-p) 'decl) + ;; If block scaning returns state, then that means + ;; there is a missing end, so value is nil. + ;; If it returns empty, then there is a matching end. + (if (matlab--scan-block-forward) + nil + t) + ;; Not on a decl, therefore just say nil, since block scanning would fail. + nil) + )))) + ) + ;; Else, just return the default. + matlab-functions-have-end)) + +(defun matlab-toggle-functions-have-end-minor-mode () + "Toggle `matlab-functions-have-end-minor-mode' only for `matlab-mode' buffers." + (matlab-functions-have-end-minor-mode) + (if (and matlab-functions-have-end-minor-mode (not (eq major-mode 'matlab-mode))) + (progn + (matlab-functions-have-end-minor-mode -1) + (error "Mode `matlab-functions-have-end' minor mode is only for MATLAB Major mode"))) + ) + +(defun matlab-indent-function-body-p () + "Non-nil if functions bodies are indented. +See `matlab-indent-function-body' variable." + (if (eq matlab-indent-function-body 'MathWorks-Standard) + ;; Dec '09 + ;; The MathWorks standard is the same as if functions have end. + matlab-functions-have-end + ;; Else, just return the variable. + matlab-indent-function-body)) + +(defun matlab-guess-function-indentation () + "Look at the current buffer and determine if functions are indented. +Setup various variables based on what we find." + (let ((st (matlab-guess-script-type)) + ) + (cond + ((not (eq st 'function)) + ;; Anything not a function should follow the mathworks standard. + (setq matlab-indent-function-body 'MathWorks-Standard) + ) + + ;; If we are guessing, keep guessing (vaguely true) + ((eq (matlab-do-functions-have-end-p t) 'guess) + (setq matlab-indent-function-body 'guess)) + + ;; Here it is a function, and there are no ends. + (t + ;; Functions in guess mode we need to find the function decl + ;; and then look at the first code line and see if it is indented + ;; to guess what to do. + (save-excursion + (goto-char (point-min)) + (matlab-find-code-line) + ;; We are likely on the fcn line. Scan to end of it. + (matlab-scan-end-of-command) + ;; Now find next code line after comments + (matlab-find-code-line) + ;; If it is indented, then we too will indent. + (setq matlab-indent-function-body + (if (> (current-indentation) 0) + (if (matlab-do-functions-have-end-p t) + ;; if it indented and we have ends, that is std. + 'MathWorks-Standard + ;; no ends but indented, not the standard. + t) + (if (matlab-do-functions-have-end-p t) + ;; have ends, not indented, force nil. + nil + ;; no ends and not indented, mw stadnard + 'MathWorks-Standard))) + ))) + )) + (defcustom matlab-fill-fudge 10 "Number of characters around `fill-column' we can fudge filling. Basically, there are places that are very convenient to fill at, but @@ -326,8 +448,6 @@ point, but it will be restored for them." '(matlab-mode-vf-functionname matlab-mode-vf-classname matlab-mode-vf-add-ends - matlab-mode-vf-block-matches-forward - matlab-mode-vf-block-matches-backward matlab-mode-vf-quiesce-buffer )))) @@ -336,44 +456,24 @@ point, but it will be restored for them." :group 'matlab :type 'integer) -;; It is time to disable this. -(defcustom matlab-vers-on-startup nil - "*If non-nil, show the version number on startup." - :group 'matlab - :type 'boolean) - -(defcustom matlab-highlight-block-match-flag t - "*Non-nil means to highlight the matching if/end/whatever. -The highlighting only occurs when the cursor is on a block start or end -keyword." - :group 'matlab - :type 'boolean) - -(defcustom matlab-show-periodic-code-details-flag nil - "*Non-nil means to show code details in the minibuffer. -This will only work if `matlab-highlight-block-match-flag' is non-nil." - :group 'matlab - :type 'boolean) - (defcustom matlab-mode-hook nil "*List of functions to call on entry to MATLAB mode." :group 'matlab :type 'hook) - (defcustom matlab-show-mlint-warnings nil "*If non-nil, show mlint warnings." :group 'matlab :type 'boolean) - (make-variable-buffer-local 'matlab-show-mlint-warnings) +(put 'matlab-show-mlint-warnings 'safe-local-variable #'booleanp) (defcustom matlab-highlight-cross-function-variables nil "*If non-nil, highlight cross-function variables." :group 'matlab :type 'boolean) - (make-variable-buffer-local 'matlab-highlight-cross-function-variables) +(put 'matlab-highlight-cross-function-variables 'safe-local-variable #'booleanp) (defcustom matlab-return-add-semicolon nil "*If non nil, check to see a semicolon is needed when RET is pressed." @@ -389,167 +489,10 @@ This will only work if `matlab-highlight-block-match-flag' is non-nil." (make-variable-buffer-local 'matlab-change-current-directory) -;; Load in the region we use for highlighting stuff. -(if (and (featurep 'custom) (fboundp 'custom-declare-variable)) - - (let ((l-region-face (if (facep 'region) 'region 'zmacs-region))) - ;; If we have custom, we can make our own special face like this - (defface matlab-region-face - (list - (list t - (list :background (face-background l-region-face) - :foreground (face-foreground l-region-face)))) - "*Face used to highlight a matlab region." - :group 'matlab)) - - ;; If we do not, then we can fake it by copying 'region. - (cond ((facep 'region) - (copy-face 'region 'matlab-region-face)) - (t - (copy-face 'zmacs-region 'matlab-region-face)))) - -(defvar matlab-unterminated-string-face 'matlab-unterminated-string-face - "Self reference for unterminated string face.") - -(defvar matlab-simulink-keyword-face 'matlab-simulink-keyword-face - "Self reference for simulink keywords.") - -(defvar matlab-nested-function-keyword-face 'matlab-nested-function-keyword-face - "Self reference for nested function/end keywords.") - -(defvar matlab-cross-function-variable-face 'matlab-cross-function-variable-face - "Self reference for cross-function variables.") - -(defvar matlab-cellbreak-face 'matlab-cellbreak-face - "Self reference for cellbreaks.") - -(defun matlab-font-lock-adjustments () - "Make adjustments for font lock. -If font lock is not loaded, lay in wait." - (if (and (featurep 'custom) (fboundp 'custom-declare-variable)) - - (progn - (defface matlab-unterminated-string-face - (list - (list t - (list :background (face-background font-lock-string-face) - :foreground (face-foreground font-lock-string-face) - :underline t))) - "*Face used to highlight unterminated strings." - :group 'matlab) - (defface matlab-simulink-keyword-face - (list - (list t - (list :background (face-background font-lock-type-face) - :foreground (face-foreground font-lock-type-face) - :underline t))) - "*Face used to highlight simulink specific functions." - :group 'matlab) - (defface matlab-nested-function-keyword-face - (list - (list t - (list :slant 'italic))) - "*Face to use for cross-function variables.") - (defface matlab-cross-function-variable-face - (list - (list t - (list :weight 'bold - :slant 'italic))) - "*Face to use for cross-function variables." - :group 'matlab) - (defface matlab-cellbreak-face - (list - (list t - (list :background (face-background font-lock-comment-face) - :foreground (face-foreground font-lock-comment-face) - :overline t - :bold t))) - "*Face to use for cellbreak %% lines.") - ) - - ;; Now, lets make the unterminated string face - (cond ((facep 'font-lock-string-face) - (copy-face 'font-lock-string-face - 'matlab-unterminated-string-face)) - (t - (make-face 'matlab-unterminated-string-face))) - (matlab-set-face-underline 'matlab-unterminated-string-face t) - - ;; Now make some simulink faces - (cond ((facep 'font-lock-type-face) - (copy-face 'font-lock-type-face 'matlab-simulink-keyword-face)) - (t - (make-face 'matlab-simulink-keyword-face))) - (matlab-set-face-underline 'matlab-simulink-keyword-face t) - - ;; Now make some nested function/end keyword faces - (cond ((facep 'font-lock-type-face) - (copy-face 'font-lock-type-face 'matlab-nested-function-keyword-face)) - (t - (make-face 'matlab-nested-function-keyword-face))) - - ;; Now make some cross-function variable faces - (cond ((facep 'font-lock-type-face) - (copy-face 'font-lock-type-face 'matlab-cross-function-variable-face)) - (t - (make-face 'matlab-cross-function-variable-face))) - (matlab-set-face-bold 'matlab-cross-function-variable-face t) - - ;; Now make some cellbreak variable faces - (cond ((facep 'font-comment-face) - (copy-face 'font-lock-comment-face 'matlab-cellbreak-face)) - (t - (make-face 'matlab-cellbreak-face))) - (matlab-set-face-bold 'matlab-cellbreak-face t) - (condition-case nil - (set-face-attribute 'matlab-cellbreak-face nil :overline t) - (error nil)) - ) - (remove-hook 'font-lock-mode-hook 'matlab-font-lock-adjustments)) - -;; Make the adjustments for font lock after it's loaded. -;; I found that eval-after-load was unreliable. -(if (featurep 'font-lock) - (matlab-font-lock-adjustments) - (add-hook 'font-lock-mode-hook 'matlab-font-lock-adjustments)) - - -;;; MATLAB mode variables ===================================================== - -;; syntax table -(defvar matlab-mode-syntax-table - (let ((st (make-syntax-table (standard-syntax-table)))) - (modify-syntax-entry ?_ "_" st) - (modify-syntax-entry ?% "<" st) - (modify-syntax-entry ?\n ">" st) - (modify-syntax-entry ?\\ "." st) - (modify-syntax-entry ?\t " " st) - (modify-syntax-entry ?+ "." st) - (modify-syntax-entry ?- "." st) - (modify-syntax-entry ?* "." st) - (modify-syntax-entry ?' "." st) - (modify-syntax-entry ?\" "." st) - (modify-syntax-entry ?/ "." st) - (modify-syntax-entry ?= "." st) - (modify-syntax-entry ?< "." st) - (modify-syntax-entry ?> "." st) - (modify-syntax-entry ?& "." st) - (modify-syntax-entry ?| "." st) - st) - "The syntax table used in `matlab-mode' buffers.") - -(defvar matlab-mode-special-syntax-table - (let ((st (copy-syntax-table matlab-mode-syntax-table))) - ;; Make _ a part of words so we can skip them better - (modify-syntax-entry ?_ "w" st) - st) - "The syntax table used when navigating blocks.") - -;; abbrev table (defvar matlab-mode-abbrev-table nil "The abbrev table used in `matlab-mode' buffers.") - (define-abbrev-table 'matlab-mode-abbrev-table ()) + ;;; Keybindings =============================================================== @@ -565,17 +508,29 @@ If font lock is not loaded, lay in wait." ;; mode map (defvar matlab-mode-map (let ((km (make-sparse-keymap))) - (define-key km [return] 'matlab-return) - (define-key km "%" 'matlab-electric-comment) - (define-key km "}" 'matlab-electric-block-comment) - (define-key km "{" 'matlab-electric-block-comment) - (define-key km "\C-c;" 'matlab-comment-region) - (define-key km "\C-c:" 'matlab-uncomment-region) - (define-key km [(control c) return] 'matlab-comment-return) + ;; Navigation Commands + (define-key km [(meta a)] 'matlab-beginning-of-command) + (define-key km [(meta e)] 'matlab-end-of-command) + ;; Insert, Fill stuff (define-key km [(control c) (control c)] 'matlab-insert-map-fcn) (define-key km [(control c) (control f)] 'matlab-fill-comment-line) (define-key km [(control c) (control j)] 'matlab-justify-line) (define-key km [(control c) (control q)] 'matlab-fill-region) + ;; Comment Stuff + (define-key km "%" 'matlab-electric-comment) + (define-key km "^" 'matlab-electric-comment) + (define-key km "}" 'matlab-electric-block-comment) + (define-key km "{" 'matlab-electric-block-comment) + (define-key km "\C-c;" 'matlab-comment-region) + (define-key km "\C-c:" 'matlab-uncomment-region) + (define-key km [(meta \;)] 'matlab-comment) + (define-key km [(meta j)] 'matlab-comment-line-break-function) + (define-key km [(control c) return] 'matlab-comment-return) + (substitute-key-definition 'comment-region 'matlab-comment-region + km global-map) ;torkel + ;; Completion + (define-key km "\M-\t" 'matlab-complete-symbol) + ;; Connecting to MATLAB Shell (define-key km [(control c) (control s)] 'matlab-shell-save-and-go) (define-key km [(control c) (control r)] 'matlab-shell-run-region) (define-key km [(meta control return)] 'matlab-shell-run-cell) @@ -583,29 +538,12 @@ If font lock is not loaded, lay in wait." (define-key km [(control c) (control t)] 'matlab-show-line-info) (define-key km [(control c) ?. ] 'matlab-shell-locate-fcn) (define-key km [(control h) (control m)] matlab-help-map) - (define-key km [(control j)] 'matlab-linefeed) - (define-key km "\M-\r" 'newline) - (define-key km [(meta \;)] 'matlab-comment) - ;;(define-key km [(meta q)] 'matlab-fill-paragraph) ; replace w/ fill-paragraph-function setting. - (define-key km [(meta a)] 'matlab-beginning-of-command) - (define-key km [(meta e)] 'matlab-end-of-command) - (define-key km [(meta j)] 'matlab-comment-line-break-function) (define-key km [(meta s)] 'matlab-show-matlab-shell-buffer) - (define-key km "\M-\t" 'matlab-complete-symbol) - (define-key km [(meta control f)] 'matlab-forward-sexp) - (define-key km [(meta control b)] 'matlab-backward-sexp) - (define-key km [(meta control q)] 'matlab-indent-sexp) - (define-key km [(meta control a)] 'matlab-beginning-of-defun) - (define-key km [(meta control e)] 'matlab-end-of-defun) - (if (string-match "XEmacs" emacs-version) - (define-key km [(control meta button1)] 'matlab-find-file-click) - (define-key km [(control meta mouse-2)] 'matlab-find-file-click)) - + (define-key km [(control meta mouse-2)] 'matlab-find-file-click) + ;; Debugger interconnect (substitute-key-definition 'read-only-mode 'matlab-toggle-read-only km global-map) - (substitute-key-definition 'comment-region 'matlab-comment-region - km global-map) ;torkel km) "The keymap used in `matlab-mode'.") @@ -614,89 +552,75 @@ If font lock is not loaded, lay in wait." (defvar matlab-mode-menu-keymap nil "Keymap used in MATLAB mode to provide a menu.") -(defun matlab-frame-init () - "Initialize Emacs menu system." - (interactive) - ;; make a menu keymap - (easy-menu-define - matlab-mode-menu - matlab-mode-map - "MATLAB menu" - '("MATLAB" - ["Start MATLAB" matlab-shell - :active (not (matlab-shell-active-p)) - :visible (not (matlab-shell-active-p)) ] - ["Switch to MATLAB" matlab-shell +;; make a menu keymap +(easy-menu-define matlab-mode-menu matlab-mode-map "MATLAB menu" + '("MATLAB" + ["Start MATLAB" matlab-shell + :active (not (matlab-shell-active-p)) + :visible (not (matlab-shell-active-p)) ] + ["Switch to MATLAB" matlab-shell + :active (matlab-any-shell-active-p) + :visible (matlab-any-shell-active-p)] + ["Save and go" matlab-shell-save-and-go + :active (matlab-any-shell-active-p) ] + ["Run Region" matlab-shell-run-region + :active (matlab-any-shell-active-p) ] + ["Run Cell" matlab-shell-run-cell + :active (matlab-any-shell-active-p) ] + ["Version" matlab-show-version t] + "----" + ["Locate MATLAB function" matlab-shell-locate-fcn + :active (matlab-shell-active-p) + :help "Run 'which FCN' in matlab-shell, then open the file in Emacs"] + ["Show M-Lint Warnings" matlab-toggle-show-mlint-warnings + :active (and (locate-library "mlint") (fboundp 'mlint-minor-mode)) + :style toggle :selected matlab-show-mlint-warnings + ] + ("Auto Fix" + ["Verify/Fix source" matlab-mode-verify-fix-file t] + ["Spell check strings and comments" matlab-ispell-strings-and-comments t] + ["Quiesce source" matlab-mode-vf-quiesce-buffer t] + ) + ("Format" + ["Justify Line" matlab-justify-line t] + ["Fill Region" matlab-fill-region t] + ["Fill Comment Paragraph" matlab-fill-paragraph + (save-excursion (matlab-comment-on-line))] + ["Join Comment" matlab-join-comment-lines + (save-excursion (matlab-comment-on-line))] + ["Comment Region" matlab-comment-region t] + ["Uncomment Region" matlab-uncomment-region t]) + ("Debug" + ["Edit File (toggle read-only)" matlab-shell-gud-mode-edit + :help "Exit MATLAB debug minor mode to edit without exiting MATLAB's K>> prompt." + :visible gud-matlab-debug-active ] + ["Add Breakpoint (ebstop in FILE at point)" gud-break + :active (matlab-shell-active-p) + :help "When MATLAB debugger is active, set break point at current M-file point"] + ["Remove Breakpoint (ebclear in FILE at point)" gud-remove + :active (matlab-shell-active-p) + :help "Show all active breakpoints in a separate buffer." ] + ["List Breakpoints (ebstatus)" gud-list-breakpoints + :active (matlab-shell-active-p) + :help "List active breakpoints."] + ["Step (dbstep in)" gud-step + :active gud-matlab-debug-active + :help "When MATLAB debugger is active, step into line"] + ["Next (dbstep)" gud-next + :active gud-matlab-debug-active + :help "When MATLAB debugger is active, step one line"] + ["Finish function (dbstep out)" gud-finish + :active gud-matlab-debug-active + :help "When MATLAB debugger is active, run to end of function"] + ["Continue (dbcont)" gud-cont + :active gud-matlab-debug-active + :help "When MATLAB debugger is active, run to next break point or finish"] + ["Evaluate Expression" matlab-shell-gud-show-symbol-value :active (matlab-any-shell-active-p) - :visible (matlab-any-shell-active-p)] - ["Save and go" matlab-shell-save-and-go - :active (matlab-any-shell-active-p) ] - ["Run Region" matlab-shell-run-region - :active (matlab-any-shell-active-p) ] - ["Run Cell" matlab-shell-run-cell - :active (matlab-any-shell-active-p) ] - ["Version" matlab-show-version t] - "----" - ["Locate MATLAB function" matlab-shell-locate-fcn - :active (matlab-shell-active-p) - :help "Run 'which FCN' in matlab-shell, then open the file in Emacs"] - ["Show M-Lint Warnings" matlab-toggle-show-mlint-warnings - :active (and (locate-library "mlint") (fboundp 'mlint-minor-mode)) - :style toggle :selected matlab-show-mlint-warnings - ] - ("Auto Fix" - ["Verify/Fix source" matlab-mode-verify-fix-file t] - ["Spell check strings and comments" matlab-ispell-strings-and-comments t] - ["Quiesce source" matlab-mode-vf-quiesce-buffer t] - ) - ("Navigate" - ["Beginning of Command" matlab-beginning-of-command t] - ["End of Command" matlab-end-of-command t] - ["Forward Block" matlab-forward-sexp t] - ["Backward Block" matlab-backward-sexp t] - ["Beginning of Function" matlab-beginning-of-defun t] - ["End of Function" matlab-end-of-defun t]) - ("Format" - ["Justify Line" matlab-justify-line t] - ["Fill Region" matlab-fill-region t] - ["Fill Comment Paragraph" matlab-fill-paragraph - (save-excursion (matlab-comment-on-line))] - ["Join Comment" matlab-join-comment-lines - (save-excursion (matlab-comment-on-line))] - ["Comment Region" matlab-comment-region t] - ["Uncomment Region" matlab-uncomment-region t] - ["Indent Syntactic Block" matlab-indent-sexp]) - ("Debug" - ["Edit File (toggle read-only)" matlab-shell-gud-mode-edit - :help "Exit MATLAB debug minor mode to edit without exiting MATLAB's K>> prompt." - :visible gud-matlab-debug-active ] - ["Add Breakpoint (ebstop in FILE at point)" gud-break - :active (matlab-shell-active-p) - :help "When MATLAB debugger is active, set break point at current M-file point"] - ["Remove Breakpoint (ebclear in FILE at point)" gud-remove - :active (matlab-shell-active-p) - :help "Show all active breakpoints in a separate buffer." ] - ["List Breakpoints (ebstatus)" gud-list-breakpoints - :active (matlab-shell-active-p) - :help "List active breakpoints."] - ["Step (dbstep in)" gud-step - :active gud-matlab-debug-active - :help "When MATLAB debugger is active, step into line"] - ["Next (dbstep)" gud-next - :active gud-matlab-debug-active - :help "When MATLAB debugger is active, step one line"] - ["Finish function (dbstep out)" gud-finish - :active gud-matlab-debug-active - :help "When MATLAB debugger is active, run to end of function"] - ["Continue (dbcont)" gud-cont - :active gud-matlab-debug-active - :help "When MATLAB debugger is active, run to next break point or finish"] - ["Evaluate Expression" matlab-shell-gud-show-symbol-value - :active (matlab-any-shell-active-p) - :help "When MATLAB is active, show value of the symbol under point."] - ["Show Stack" mlg-show-stack - :active gud-matlab-debug-active - :help "When MATLAB debugger is active, show the stack in a buffer."] + :help "When MATLAB is active, show value of the symbol under point."] + ["Show Stack" mlg-show-stack + :active gud-matlab-debug-active + :help "When MATLAB debugger is active, show the stack in a buffer."] ;;; Advertise these more if we can get them working w/ gud's frame show. ;;; ["Up Call Stack (dbup)" gud-up ;;; :active gud-matlab-debug-active @@ -704,311 +628,148 @@ If font lock is not loaded, lay in wait." ;;; ["Down Call Stack (dbdown)" gud-down ;;; :active gud-matlab-debug-active ;;; :help "When MATLAB debugger is active and at break point, go down a frame"] - ["Quit debugging (dbquit)" gud-stop-subjob - :active gud-matlab-debug-active - :help "When MATLAB debugger is active, stop debugging"] - ) + ["Quit debugging (dbquit)" gud-stop-subjob + :active gud-matlab-debug-active + :help "When MATLAB debugger is active, stop debugging"] + ) -;; TODO - how to autoload these? Do we want this menu? -;; ("Insert" -;; ["Complete Symbol" matlab-complete-symbol t] -;; ["Comment" matlab-comment t] -;; ["if end" tempo-template-matlab-if t] -;; ["if else end" tempo-template-matlab-if-else t] -;; ["for end" tempo-template-matlab-for t] -;; ["switch otherwise end" tempo-template-matlab-switch t] -;; ["Next case" matlab-insert-next-case t] -;; ["try catch end" tempo-template-matlab-try t] -;; ["while end" tempo-template-matlab-while t] -;; ["End of block" matlab-insert-end-block t] -;; ["Function" tempo-template-matlab-function t] -;; ["Stringify Region" matlab-stringify-region t] -;; ) - ("Customize" -; ["Auto Fill Counts Elipsis" -; (lambda () (setq matlab-fill-count-ellipsis-flag -; (not matlab-fill-count-ellipsis-flag))) -; :style toggle :selected 'matlab-fill-count-ellipsis-flag] - ["Indent Function Body" - (setq matlab-indent-function-body (not (matlab-indent-function-body-p))) - :style toggle :selected matlab-indent-function-body] - ["Functions Have end" - matlab-toggle-functions-have-end - :style toggle :selected matlab-functions-have-end] - ["Verify File on Save" - (setq matlab-verify-on-save-flag (not matlab-verify-on-save-flag)) - :style toggle :selected matlab-verify-on-save-flag] - ["Auto Fill does Code" - (setq matlab-fill-code (not matlab-fill-code)) - :style toggle :selected matlab-fill-code ] - ["Periodic Code Details" - (setq matlab-show-periodic-code-details-flag - (not matlab-show-periodic-code-details-flag)) - :style toggle :selected matlab-show-periodic-code-details-flag ] - ;; ["Highlight Matching Blocks" - ;; (matlab-enable-block-highlighting) - ;; :style toggle :selected (member 'matlab-start-block-highlight-timer - ;; post-command-hook) ] - ["Highlight Cross-Function Variables" - matlab-toggle-highlight-cross-function-variables - :active (locate-library "mlint") - :style toggle :selected matlab-highlight-cross-function-variables - ] - ["Add Needed Semicolon on RET" - (setq matlab-return-add-semicolon (not matlab-return-add-semicolon)) - :style toggle :selected matlab-return-add-semicolon - ] - ["Customize" (customize-group 'matlab) - (and (featurep 'custom) (fboundp 'custom-declare-variable)) - ] - ) - "----" - ["Run M Command" matlab-shell-run-command (matlab-shell-active-p)] - ["Describe Command" matlab-shell-describe-command (matlab-shell-active-p)] - ["Describe Variable" matlab-shell-describe-variable (matlab-shell-active-p)] - ["Command Apropos" matlab-shell-apropos (matlab-shell-active-p)] - )) - (easy-menu-add matlab-mode-menu matlab-mode-map)) + ;; TODO - how to autoload these? Do we want this menu? + ;; ("Insert" + ;; ["Complete Symbol" matlab-complete-symbol t] + ;; ["Comment" matlab-comment t] + ;; ["if end" tempo-template-matlab-if t] + ;; ["if else end" tempo-template-matlab-if-else t] + ;; ["for end" tempo-template-matlab-for t] + ;; ["switch otherwise end" tempo-template-matlab-switch t] + ;; ["Next case" matlab-insert-next-case t] + ;; ["try catch end" tempo-template-matlab-try t] + ;; ["while end" tempo-template-matlab-while t] + ;; ["End of block" matlab-insert-end-block t] + ;; ["Function" tempo-template-matlab-function t] + ;; ["Stringify Region" matlab-stringify-region t] + ;; ) + ("Customize" + ["Indent Function Body" + (setq matlab-indent-function-body (not (matlab-indent-function-body-p))) + :style toggle :selected matlab-indent-function-body] + ["Functions Have end" + matlab-toggle-functions-have-end + :style toggle :selected matlab-functions-have-end] + ["Verify File on Save" + (setq matlab-verify-on-save-flag (not matlab-verify-on-save-flag)) + :style toggle :selected matlab-verify-on-save-flag] + ["Auto Fill does Code" + (setq matlab-fill-code (not matlab-fill-code)) + :style toggle :selected matlab-fill-code ] + ["Highlight Cross-Function Variables" + matlab-toggle-highlight-cross-function-variables + :active (locate-library "mlint") + :style toggle :selected matlab-highlight-cross-function-variables + ] + ["Add Needed Semicolon on RET" + (setq matlab-return-add-semicolon (not matlab-return-add-semicolon)) + :style toggle :selected matlab-return-add-semicolon + ] + ["Customize" (customize-group 'matlab) + (and (featurep 'custom) (fboundp 'custom-declare-variable)) + ] + ) + "----" + ["Run M Command" matlab-shell-run-command (matlab-shell-active-p)] + ["Describe Command" matlab-shell-describe-command (matlab-shell-active-p)] + ["Describe Variable" matlab-shell-describe-variable (matlab-shell-active-p)] + ["Command Apropos" matlab-shell-apropos (matlab-shell-active-p)] + )) +(easy-menu-add matlab-mode-menu matlab-mode-map) ;;; Font Lock : Character Vectors, Strings and Comments ================================ ;; ;; Combine these, but do all the matching internally instead of using regexp ;; because it's just too complex for a regular expression. +(defface matlab-region-face + '((t :inherit region)) + "*Face used to highlight a matlab region." + :group 'matlab) -(defvar matlab-string-start-regexp "\\(^\\|[^]})a-zA-Z0-9_.'\"]\\)" - "Regexp used to represent the character before the char vector or string scalars. -The ' character has restrictions on what starts a string which is needed -when attempting to understand the current context.") +(defvar matlab-unterminated-string-face 'matlab-unterminated-string-face + "Self reference for unterminated string face.") -(defvar matlab-font-lock-string-start-regexp (concat matlab-string-start-regexp "\\(['\"]\\)") - "Regexp used by font lock to find the beginning of a char vector or string scalar.") +(defvar matlab-commanddual-string-face 'matlab-commanddual-string-face + "Self reference for command dual string face.") -(defvar matlab-font-lock-string-and-comment-start-regexp (concat matlab-font-lock-string-start-regexp - "\\|\\(%\\)\\|\\(\\.\\.\\.\\)") - "Starting matcher for allstring comment font lock.") +(defvar matlab-simulink-keyword-face 'matlab-simulink-keyword-face + "Self reference for simulink keywords.") -(defun matlab-test-allstring-comment-match () - "Text command for the allstring font locker." - (interactive) - ;(beginning-of-line) - (matlab-font-lock-allstring-comment-match-normal (point-max)) - (goto-char (match-beginning 0)) - (call-interactively 'set-mark-command) - (goto-char (match-end 0))) +(defvar matlab-nested-function-keyword-face 'matlab-nested-function-keyword-face + "Self reference for nested function/end keywords.") -(defun matlab-font-lock-allstring-comment-match-normal (limit) - "When font locking strings, call this function for normal character vectors. -Argument LIMIT is the maximum distance to scan." - (when (and (< (point) limit) - (re-search-forward matlab-font-lock-string-and-comment-start-regexp - (if (eq limit (point-max)) - (- limit 1) ;; consider case of "..." at EOF - limit) - t)) +(defvar matlab-cross-function-variable-face 'matlab-cross-function-variable-face + "Self reference for cross-function variables.") - ;; We might match a comment, string or unterminated string. - (let ((strchar (preceding-char)) - (b0 (or (match-beginning 2) (match-beginning 3) (match-end 4))) - (bs nil) - (es nil) - (bu nil) - (eu nil) - (bc nil) - (ec nil) - (done nil) - (searchlim (point-at-eol))) +(defvar matlab-cellbreak-face 'matlab-cellbreak-face + "Self reference for cellbreaks.") - ;; Identify the thing we just found, and do different things based on that. - ;; +(defvar matlab-math-face 'matlab-math-face + "Self reference for math.") - ;; Comments and elipsis go to the end of the line, making this part simple - (if (or (eq strchar ?%) (eq strchar ?.)) - (progn - (setq bc b0 - ec (point-at-eol)) - ) +(defface matlab-unterminated-string-face + '((t :inherit font-lock-string-face + :underline t)) + "*Face used to highlight unterminated strings." + :group 'matlab) - ;; Not a comment, must be a string or charvec +(defface matlab-commanddual-string-face + '((t :inherit font-lock-string-face + :slant italic)) + "*Face used to highlight command dual string equivalent." + :group 'matlab) - ;; Scan to end of string by looking at every matching - ;; string character and deciding what it means. - (while (and (not done) - (re-search-forward "['\"]" searchlim t)) - (goto-char (match-end 0)) - (if (eq (preceding-char) strchar) - ;; Same type of string - (if (eq (following-char) strchar) - ;; This is a quoted quote. Skip it and keep going. - (forward-char 1) - ;; solo quote, end of string - (setq bs b0 - es (point) - done t)) - ;; The other type of string - just keep going. - nil)) +(defface matlab-simulink-keyword-face + '((t :inherit font-lock-builtin-face + :underline t)) + "*Face used to highlight simulink specific functions." + :group 'matlab) - ;; If not done, unterminated - (if (not done) - (setq bu b0 - eu searchlim)) - ) +(defface matlab-nested-function-keyword-face + '((t :inherit font-lock-keyword-face + :slant italic)) + "*Face to use for cross-function variables.") - ;; Fake out some match data - (set-match-data - (list - b0 (or es eu ec) - bs es ; matched string - bu eu ; unterminated string - bc ec ; comment - )) +(defface matlab-cross-function-variable-face + '((t :weight bold + :slant italic)) + "*Face to use for cross-function variables." + :group 'matlab) - ;; Move to the end - (goto-char (or es eu ec)) +(defface matlab-cellbreak-face + '((t :inherit font-lock-comment-face + :overline t + :bold t)) + "*Face to use for cellbreak %% lines.") - ;; Successful string - t))) +(defface matlab-ignored-comment-face + '((t :inherit font-lock-comment-face + :slant italic)) + "*Face to use for ignored comments. +Ignored comments are lines that start with '% $$$' or '%^'.") -;;; Font Lock Comment and Unreachable Code Matchers -;; -(defvar font-lock-beg) (defvar font-lock-end) ; quiet compiler. - -(defun matlab-font-lock-extend-region () - "Called by font-lock to extend the region if we are in a multi-line block." - ;; Only deal with block comments for now. - - (let* ((pos (matlab-ltype-block-comm t)) - (flb font-lock-beg) - (fle font-lock-end)) - (when pos - (setq font-lock-beg (min font-lock-beg (car pos)) - font-lock-end (max font-lock-end (cdr pos)))) - - (if (and (eq font-lock-beg flb) - (eq font-lock-end fle)) - ;; We didn't change anything. - nil - - ;; We made a change - t))) - -(defconst matlab-block-comment-start-re "^\\s-*%{\\s-*$" - "Regexp that matches the beginning of a block comment. -Block comment indicators must be on a line by themselves.") - -(defun matlab-ltype-block-comment-start () - "Return non-nil if the current line is a block comment start." - (save-excursion - (beginning-of-line) - (looking-at matlab-block-comment-start-re))) - -(defconst matlab-block-comment-end-re "^\\s-*%}\\s-*$" - "Regexp that matches the end of a block comment. -Block comment indicators must be on a line by themselves.") - -(defun matlab-ltype-block-comment-end () - "Return non-nil if the current line is a block comment start." - (save-excursion - (beginning-of-line) - (looking-at matlab-block-comment-end-re))) - - -(defun matlab-find-block-comments (limit) - "Find code that is commented out with %{ until %}. -Argument LIMIT is the maximum distance to search." - (if (and (< (point) limit) - (re-search-forward matlab-block-comment-start-re limit t)) - (let ((b1 (match-beginning 0)) - (e1 (match-end 0)) - (b2 nil) (e2 nil) - (b3 nil) (e3 nil)) - (goto-char b1) - (if (and (not (bolp)) - (progn - (forward-char -1) - (matlab-cursor-in-string-or-comment))) - (progn - (goto-char e1) ;; skip over this one. - nil) - ;; Else, find the end. We will certainly be in - ;; a comment, so no need to check on the end. - (setq b2 (re-search-forward matlab-block-comment-end-re limit t)) - (if (not b2) - (progn - ;; No end ? Let's tell font-lock to just go - ;; to point-at-eol can call it done. - (goto-char e1) - (set-match-data - (list b1 (point-max) - b1 (point-max) - b1 e1 - (point-max) (point-max) - )) - (goto-char (point-max)) - t) - - ;; We have a match. Return that region. - (setq b2 (match-beginning 0) - e2 (match-end 0)) - (set-match-data - (list b1 e2 ; full match - b1 e2 ; the full comment - b1 e1 ; the block start - b2 e2 ; the block end - )) - (goto-char e2); move to end - t - ))))) - -(defun matlab-find-unreachable-code (limit) - "Find code that is if'd out with if(0) or if(false), and mark it as a comment. -The if(0) and else/end construct should be highlighted differently. -Argument LIMIT is the maximum distance to search." - (if (and (< (point) limit) - (re-search-forward - "\\<\\(if\\>\\s-*(?\\s-*\\(0\\|false\\)\\s-*)?$\\)" - limit t)) - (let ((b1 (match-beginning 1)) - (e1 (match-end 1)) - (b2 nil) (e2 nil) - (b3 nil) (e3 nil)) - (goto-char b1) - (condition-case nil - (progn - ;; Go forward over the matlab sexp. Include scanning - ;; for ELSE since parts of the ELSE block are not - ;; `commented out'. - (matlab-forward-sexp t) - (forward-word -1) - ;; Is there an ELSE in this block? - (if (looking-at (matlab-block-mid-re)) - (progn - (setq b3 (match-beginning 0) - e3 (match-end 0)) - ;; Now find the REAL end. - (matlab-forward-sexp) - (forward-word -1))) - ;; End of block stuff - (if (looking-at (matlab-block-end-re)) - (progn - (setq b2 (match-beginning 0) - e2 (match-end 0)) - ;; make sure something exists... - (if (not b3) (setq b3 b2 e3 e2))) - (error "Eh?")) - ;; Ok, build up some match data. - (set-match-data - (list b1 e2 ;the real deal. - b1 e1 ;if (0) - b2 e2 ;end - b3 e3 ;else (if applicable.) - b1 e3)) ;body commented out. - t) - (error nil))))) +(defface matlab-pragma-face + '((t :inherit font-lock-comment-face + :bold t)) + "*Face to use for cellbreak %% lines.") +(defface matlab-math-face + '((t :inherit font-lock-constant-face + :slant italic)) + "*Face to use for cellbreak %% lines.") ;;; Font Lock MLINT data highlighting +;; TODO - THE BELOW LOOKS BROKEN TO ME. +;; - these are used in font lock, but are hand adding overlays +;; - and returning no matches - but the font lock keywords try to add +;; - a font. NEEDS FIX (defun matlab-font-lock-nested-function-keyword-match (limit) "Find next nested function/end keyword for font-lock. Argument LIMIT is the maximum distance to search." @@ -1060,27 +821,39 @@ Argument LIMIT is the maximum distance to search." nil ;; no matches, stop ))) -(defcustom matlab-keyword-list '("global" "persistent" "for" "parfor" "while" - "spmd" "if" "elseif" "else" - "return" "break" "continue" - "switch" "case" "otherwise" "try" - "catch" "tic" "toc" - ;; MCOS keywords - "properties" "methods" "enumeration" "events" - "arguments" - ) - "List of keywords for MATLAB used in highlighting. -Customizing this variable is only useful if `regexp-opt' is available." - :group 'matlab - :type '(repeat (string :tag "Keyword: "))) -(defcustom matlab-handle-graphics-list '("figure" "axes" "axis" "line" - "surface" "patch" "text" "light" - "image" "set" "get" "uicontrol" - "uimenu" "uitoolbar" - "uitoggletool" "uipushtool" - "uicontext" "uicontextmenu" - "setfont" "setcolor") +(defcustom matlab-hg-primitives-list + '(;; start with basic / primitive objects + "figure" "axes" "line" "surface" "patch" "text" "light" "image" "imagesc" + "rectangle" "animatedline" + ;; core utilities + "set" "get" "reset" "copyobj" "findobj" "cla" "clf" "shg" + ;; popular helpers + "axis" "hold" "title" "xlabel" "ylabel" "zlabel" "xlim" "ylim" "zlim" "rlim" "thetalim" + "lighting" "shading" "material" + ;; popular cartesian charts + "plot" "plot3" "semilogx" "semilogy" "loglog" "scatter" "scatter3" "stackedplot" + "area" "errorbar" "bubblechart" "bubblechart3" "swarmchart" "swarmchart3" "spy" + "histogram" "histogram2" "wordcloud" "bubblecloud" "heatmap" "parallelplot" + "bar" "barh" "bar3" "bar3h" "stem" "stairs" "quiver" "quiver3" "stem3" + "contour" "contourf" "contour3" "contourslice" "fcontour" + ;; 3D + "surf" "surfc" "surfl" "ribbon" "pcolor" "mesh" "meshc" "meshz" "waterfall" + ;; anim + "comet" "comet3" + ;; polar + "polarplot" "polarscatter" "polarhistogram" "polarbubblechart" + ;; geographic + "goeplot" "geoscatter" "geobubble" "geodensity" + ;; function plots + "fplot" "fplot3" "fimplicit" "fsurf" "fimplicit3" + ;; misc tools + "legend" "colorbar" "tiledlayout" "nexttile" "subplot" "annotation" + ;; Componnents + "uicontrol" "uimenu" "uitoolbar" "uitoggletool" "uipushtool" "uicontext" "uicontextmenu" + ;; misc dialogs + "uisetfont" "uisetcolor" "uigetfile" "uiputfile") + "List of handle graphics functions used in highlighting. Customizing this variable is only useful if `regexp-opt' is available." :group 'matlab @@ -1107,7 +880,7 @@ Customizing this variable is only useful if `regexp-opt' is available." (defcustom matlab-constants-keyword-list - '("eps" "pi" "inf" "Inf" "nan" "NaN" "ans" "i" "j" "NaT" "true" "false") + '("eps" "pi" "flintmax" "inf" "Inf" "nan" "NaN" "ans" "i" "j" "NaT" "true" "false") "List of constants and special variables in MATLAB." :group 'matlab :type '(repeat (string :tag "Debug Keyword: "))) @@ -1115,100 +888,201 @@ Customizing this variable is only useful if `regexp-opt' is available." (defun matlab-font-lock-regexp-opt (keywordlist) "Create a font-lock usable KEYWORDLIST matching regular expression. Uses `regex-opt' if available. Otherwise creates a 'dumb' expression." - (concat "\\<\\(" + (concat "\\_<\\(" (if (fboundp 'regexp-opt) (regexp-opt keywordlist) (mapconcat (lambda (s) s) keywordlist "\\|")) - "\\)\\>")) + "\\)\\_>")) -;; font-lock keywords -(defvar matlab-font-lock-keywords +;;; Font lock keyword handlers +;; +(defun matlab-font-lock-basic-keyword-match (limit) + "Font lock matcher for basic keywords. +Fails to match when keywords show up as variables, etc." + (matlab--scan-next-keyword 'fl-simple limit)) + +(defun matlab-font-lock-vardecl-keyword-match (limit) + "Font lock matcher for mcos keywords. +Fails to match when keywords show up as variables, etc." + (matlab--scan-next-keyword 'vardecl limit)) + +(defvar matlab-fl-anchor-keyword nil) +(defun matlab-font-lock-mcos-keyword-match (limit) + "Font lock matcher for mcos keywords. +Fails to match when keywords show up as variables, etc." + (when (and (eq matlab-functions-have-end 'class) + (setq matlab-fl-anchor-keyword + (matlab--scan-next-keyword 'mcos limit))) + (save-match-data + ;; Skip over attributes. + (when (looking-at "\\s-*(") (forward-sexp 1))) + t)) + +(defun matlab-font-lock-args-keyword-match (limit) + "Font lock matcher for mcos keywords. +Fails to match when keywords show up as variables, etc." + (setq matlab-fl-anchor-keyword + (matlab--scan-next-keyword 'args limit))) + +(defvar font-lock-beg) (defvar font-lock-end) ; quiet compiler. +(defun matlab-font-lock-extend-region () + "Called by font-lock to extend the region for multiline expressions. +Supports expressions like arguments and property blocks with anchored +color support." + (save-excursion + (let* ((flb font-lock-beg) + (fle font-lock-end) + (tmp (matlab--scan-block-backward-up (window-start))) + (blockmatch (when (not tmp) (matlab--mk-keyword-node)))) + (when (and (member (nth 1 blockmatch) '("properties" "events" "arguments")) + (matlab--valid-keyword-node blockmatch)) + (setq font-lock-beg (min font-lock-beg (point-at-bol))) + (when (not (matlab--scan-next-keyword 'all (window-end))) + (setq font-lock-end (max font-lock-end (point-at-eol))))) + + (if (and (eq font-lock-beg flb) + (eq font-lock-end fle)) + ;; We didn't change anything. + nil + + ;; We made a change + t)))) + +(defvar ml-fl-anchor-limit nil) +(defun matlab-font-lock-anchor-set-end-limit () + "Set the end limit for anchored matchers." + (save-excursion + (save-match-data + ;; next keyword is faster, plus if someone is in the middle of typing + ;; a new block, prevents going too far into the distance. + (matlab--scan-next-keyword 'all (point-max)) + (forward-word -1) + (setq ml-fl-anchor-limit (point))))) + +(defun matlab-font-lock-anchor-clear-end-limit () + "Clear the end limit for anchored matchers." + (setq ml-fl-anchor-limit nil)) + +(defun matlab-font-lock-anchor-variable-match (limit) + "After finding a keyword like PROPERTIES or ARGUMENTS, match vars. +This matcher will handle a range of variable features." + (when (member (nth 1 matlab-fl-anchor-keyword) + '("properties" "events" "arguments")) + (let* ((match (re-search-forward "\\(?:^\\|[,;]\\)\\s-+\\(\\(?:\\w+\\|\\.\\)+\\)\\_>" ml-fl-anchor-limit t)) + ;; Save this match so we can do a 2nd anchored search for a data type. + (md1 (list (match-beginning 1) (match-end 1))) + (tm (looking-at + "\\(\\(?:\\s-*([^\n\)]+)\\s-*\\|\\s-+\\)?\\(?:\\w+\\|\\.\\)*\\)\\s-*\\($\\|[.%{=]\\)")) + (tm1 (if tm (list (match-beginning 1) (match-end 1)) + ;; The below is a cheat to not highlight anything but + ;; still supply the match data for this optional piece. + (list (nth 1 md1) (nth 1 md1)))) + (newmdata (append md1 md1 tm1))) + (when match + (goto-char (point-at-eol)) + (set-match-data newmdata) + t)))) + +;;; Font Lock keyword handling +;; +;; Many parts of the keyword handling are shared with matlab-shell. +;; The matlab based variables here are divided up between generic keywords +;; and keywords only for M files. This means the M shell won't highlight +;; some syntaxes like classdef stuff even though someone might paste them in. +;; +;; matlab-*-keywords -- MATLAB Files or Shell +;; matlab-file-*-keywords -- MATLAB Files only + +(defconst matlab-basic-font-lock-keywords (list - ;; charvec and string quote chars are also used as transpose, but only if directly - ;; after characters, numbers, underscores, or closing delimiters. - '(matlab-font-lock-allstring-comment-match-normal - (1 font-lock-string-face nil t) - (2 matlab-unterminated-string-face nil t) - (3 font-lock-comment-face nil t)) - ;; Various pragmas should be in different colors. - ;; I think pragmas are always lower case? - '("%#\\([a-z]+\\)" (1 'bold prepend)) ;; General keywords - (list (matlab-font-lock-regexp-opt matlab-keyword-list) - '(0 font-lock-keyword-face)) - ;; The end keyword is only a keyword when not used as an array - ;; dereferencing part. - '("\\(^\\|[;,]\\)[ \t]*\\(end\\)\\b" - 2 (if (matlab-valid-end-construct-p) font-lock-keyword-face nil)) - ;; How about unreachable code? MUST BE AFTER KEYWORDS in order to - ;; get double-highlighting. - '(matlab-find-unreachable-code - (1 'underline prepend) ;if part - (2 'underline prepend) ;end part - (3 'underline prepend) ;else part (if applicable) - (4 font-lock-comment-face prepend) ;commented out part. - ) - ;; block comments need to be commented out too! - '(matlab-find-block-comments - (1 font-lock-comment-face prepend) ; commented out - (2 'underline prepend) - (3 'underline prepend) ;the comment parts - ) - ;; Cell mode breaks get special treatment - '("^\\s-*\\(%%[^\n]*\n\\)" (1 matlab-cellbreak-face append)) - ;; Highlight cross function variables - '(matlab-font-lock-cross-function-variables-match - (1 matlab-cross-function-variable-face prepend)) - ;; Highlight nested function/end keywords - '(matlab-font-lock-nested-function-keyword-match - (0 matlab-nested-function-keyword-face prepend)) - ;; The global keyword defines some variables. Mark them. - '("^\\s-*global\\s-+" - ("\\(\\w+\\)\\(\\s-*=[^,; \t\n]+\\|[, \t;]+\\|$\\)" - nil nil (1 font-lock-variable-name-face))) + '(matlab-font-lock-basic-keyword-match + (0 font-lock-keyword-face)) ;; Handle graphics stuff (list - (matlab-font-lock-regexp-opt matlab-handle-graphics-list) - '(0 font-lock-type-face)) + (matlab-font-lock-regexp-opt matlab-hg-primitives-list) + '(0 font-lock-builtin-face)) (list ;; How about a few matlab constants such as pi, infinity, and sqrt(-1)? (matlab-font-lock-regexp-opt matlab-constants-keyword-list) - 1 font-lock-constant-face) + 1 'matlab-math-face) ;; Imaginary number support - '("\\<[0-9]\\.?\\(i\\|j\\)\\>" 1 font-lock-reference-face) + '("\\<[0-9]\\.?\\(i\\|j\\)\\_>" 1 font-lock-reference-face) ) - "Expressions to highlight in MATLAB mode.") + "Basic Expressions to highlight in MATLAB mode or shell.") +(defconst matlab-file-basic-font-lock-keywords + (append + matlab-basic-font-lock-keywords + '(;; MCOS keywords like properties, methods, events + (matlab-font-lock-mcos-keyword-match + (1 font-lock-keyword-face)) + ;; ARGUMENTS keyword + (matlab-font-lock-args-keyword-match + (1 font-lock-keyword-face)) + ;; Highlight cross function variables + (matlab-font-lock-cross-function-variables-match + (1 matlab-cross-function-variable-face prepend)) + ;; Highlight nested function/end keywords + (matlab-font-lock-nested-function-keyword-match + (0 matlab-nested-function-keyword-face prepend)) + )) + "Basic Expressions to highlight in MATLAB Files.") -(defconst matlab-function-arguments - "\\(([^)]*)\\)?\\s-*\\([,;\n%]\\|$\\)") +(defconst matlab-fl-opt-continuation "\\s<\\S>+\\s>") +(defconst matlab-fl-opt-whitespace (concat "\\s-*\\(?:" + matlab-fl-opt-continuation + "\\)?\\s-*")) -(defvar matlab-function-font-lock-keywords +(defconst matlab-fl-fcn-key "^\\s-*function\\_>") +(defconst matlab-fl-return-args "\\(\\[[^]]*\\]\\|\\sw+\\)") +(defconst matlab-fl-fcn-name "\\(?:[sg]et\\.\\)?\\sw+") +(defconst matlab-fl-fcn-args "\\(?:(\\|$\\|\\s<\\)" ) + +(defconst matlab-function-font-lock-keywords (list ;; defining a function, a (possibly empty) list of assigned variables, ;; function name, and an optional (possibly empty) list of input variables - (list (concat "^\\s-*\\(function\\)\\>[ \t\n.]*" - "\\(\\[[^]]*\\]\\|\\sw+\\)[ \t\n.]*" - "=[ \t\n.]*\\(\\sw+\\)[ \t\n.]*" - matlab-function-arguments) - '(1 font-lock-keyword-face append) - '(2 font-lock-variable-name-face append) - '(3 font-lock-function-name-face append)) + (list (concat matlab-fl-fcn-key matlab-fl-opt-whitespace + matlab-fl-return-args matlab-fl-opt-whitespace + "=" matlab-fl-opt-whitespace + "\\(" matlab-fl-fcn-name "\\)" matlab-fl-opt-whitespace + matlab-fl-fcn-args) + '(1 font-lock-variable-name-face append) + '(2 font-lock-function-name-face prepend)) ;; defining a function, a function name, and an optional (possibly ;; empty) list of input variables - (list (concat "^\\s-*\\(function\\)[ \t\n.]+" - "\\(\\sw+\\)[ \t\n.]*" - matlab-function-arguments) - '(1 font-lock-keyword-face append) - '(2 font-lock-function-name-face append)) + (list (concat matlab-fl-fcn-key matlab-fl-opt-whitespace + "\\(" matlab-fl-fcn-name "\\)" matlab-fl-opt-whitespace + matlab-fl-fcn-args) + '(1 font-lock-function-name-face prepend)) ;; Anchor on the function keyword, highlight params - (list (concat "^\\s-*function\\>[ \t\n.]*" - "\\(\\(\\[[^]]*\\]\\|\\sw+\\)[ \t\n.]*=[ \t\n.]*\\)?" - "\\sw+\\s-*(") - '("\\s-*\\(\\sw+\\)\\s-*[,)]" - (save-excursion (matlab-end-of-command) (point)) - nil - (1 font-lock-variable-name-face))) + (list (concat matlab-fl-fcn-key matlab-fl-opt-whitespace + "\\(" matlab-fl-return-args matlab-fl-opt-whitespace + "=" matlab-fl-opt-whitespace + "\\)?" + matlab-fl-fcn-name matlab-fl-opt-whitespace + "(") + (list (concat matlab-fl-opt-whitespace "\\(\\sw+\\)" + matlab-fl-opt-whitespace "[,)]") + '(save-excursion + (condition-case nil + (matlab-scan-end-of-command) + (error (point-at-eol)))) + nil + '(1 font-lock-variable-name-face))) + ;; ARGUMENTS have variables to highlight + '(matlab-font-lock-args-keyword-match + (matlab-font-lock-anchor-variable-match ;; matcher fcn + (matlab-font-lock-anchor-set-end-limit) ;; pre forms + (matlab-font-lock-anchor-clear-end-limit) ;; post forms + (1 font-lock-variable-name-face t) + (2 font-lock-type-face t) + )) + ;; VARDECL keywords + '(matlab-font-lock-vardecl-keyword-match + ("\\(\\w+\\)\\(\\s-*=[^,; \t\n]+\\|[, \t;]+\\|$\\)" + nil nil (1 font-lock-variable-name-face))) ;; I like variables for FOR loops '("\\<\\(\\(?:par\\)?for\\)\\s-+\\(\\sw+\\)\\s-*=\\s-*\ \\(\\([^\n,;%(]+\\|([^\n%)]+)\\)+\\)" @@ -1216,11 +1090,11 @@ Uses `regex-opt' if available. Otherwise creates a 'dumb' expression." (2 font-lock-variable-name-face append) (3 font-lock-reference-face append)) ;; Items after a switch statements are cool - '("\\<\\(case\\|switch\\)\\s-+\\({[^}\n]+}\\|[^,%\n]+\\)" - (1 font-lock-keyword-face) (2 font-lock-reference-face)) + '("\\_<\\(case\\|switch\\)\\_>\\s-+\\({[^}\n]+}\\|[^,%\n]+\\)" + (2 font-lock-reference-face)) ;; set_param and waitfor have input variables that can be highlighted. - (list (concat "\\<" matlab-indent-past-arg1-functions "\\s-*") - '("(\\s-*\\(\\w+\\)\\s-*\\(,\\|)\\)" nil nil + (list (concat matlab-indent-past-arg1-functions "\\s-*") + '("(\\s-*\\(\\(?:\\w\\|\\.\\)+\\)\\s-*\\(,\\|)\\)" nil nil (1 font-lock-variable-name-face))) ) "List of font lock keywords for stuff in functions.") @@ -1229,13 +1103,13 @@ Uses `regex-opt' if available. Otherwise creates a 'dumb' expression." "\\s-*\\(?2:(\\([^)]+\\))\\|\\)" "Regular expression for matching an attributes block.") -(defvar matlab-class-font-lock-keywords +(defconst matlab-file-class-font-lock-keywords (list ;; Classdefs keyword and the class name - (list (concat "^\\s-*\\(classdef\\)" + (list (concat "^\\s-*\\(classdef\\)\\_>" matlab-class-attributes-list-re - "\\s-+\\(?3:\\sw+\\)") - '(1 font-lock-keyword-face append) + "\\s-*\\(?3:\\sw+\\)") + ;; '(1 font-lock-keyword-face append) - handled as keyword '(3 font-lock-function-name-face) ) ;; Classdef anchor for highlighting all the base classes in inherits from @@ -1249,36 +1123,35 @@ Uses `regex-opt' if available. Otherwise creates a 'dumb' expression." '("\\(\\sw+\\)\\s-*\\(=\\s-*[^,)]+\\)?" nil nil (1 font-lock-type-face) )) - ;; Properties can have a type syntax after them - '("^\\s-*\\w+\\s-*\\(([:0-9,]+)\\s-*[^{=\n]+\\)" - (1 font-lock-type-face nil nil)) - ;; Properties blocks are full of variables - '("^\\s-*\\(properties\\|events\\|arguments\\)\\>" - ("^\\s-*\\(\\sw+\\)\\>" ;; This part matches the variable - ;; extend region to match in - (save-excursion (matlab-forward-sexp nil t) (beginning-of-line) (point)) - nil - (1 font-lock-variable-name-face t)) - ) + ;; PROPERY, EVENTS, etc have variables to highlight + '(matlab-font-lock-mcos-keyword-match + (matlab-font-lock-anchor-variable-match ;; matcher fcn + (matlab-font-lock-anchor-set-end-limit) ;; pre forms + (matlab-font-lock-anchor-clear-end-limit) ;; post forms + (1 font-lock-variable-name-face t) + (2 font-lock-type-face t) + )) + ) "List of font-lock keywords used when an MATLAB file contains a class.") -(defvar matlab-gaudy-font-lock-keywords +(defconst matlab-file-gaudy-font-lock-keywords (append - matlab-font-lock-keywords + matlab-basic-font-lock-keywords + matlab-file-basic-font-lock-keywords matlab-function-font-lock-keywords - matlab-class-font-lock-keywords + matlab-file-class-font-lock-keywords ) "Expressions to highlight in MATLAB mode.") -(defvar matlab-really-gaudy-font-lock-keywords +(defconst matlab-really-gaudy-font-lock-keywords (append - matlab-gaudy-font-lock-keywords (list ;; Since it's a math language, how bout dem symbols? - '("\\([<>~]=?\\|\\.[/*^']\\|==\\|\\\\|[-!^&|*+\\/~:]\\)" - 1 font-lock-type-face) - '("[]A-Za-z0-9_\"})']\\('+\\)" 1 font-lock-type-face) + '("\\([<>~=]=\\|\\.[/\\*^'?]\\|\\_<\\(?:\\\\|[-<>!?^&|*+\\/~:@]\\)" + 1 font-lock-builtin-face) + ;; highlight transpose + '("[]A-Za-z0-9_\"})']\\('+\\)" 1 font-lock-builtin-face) ;; How about references in the HELP text. (list (concat "^" matlab-comment-line-s "\\s-*" "\\(\\([A-Z]+\\s-*=\\s-+\\|\\[[^]]+]\\s-*=\\s-+\\|\\)" @@ -1296,16 +1169,17 @@ Uses `regex-opt' if available. Otherwise creates a 'dumb' expression." '(0 'bold)) ;; Simulink functions (list (matlab-font-lock-regexp-opt matlab-simulink-keywords) - ;;(list (list (concat "\\<\\(\\([sg]et_param\\|sim\\([gs]et\\)?\\|" - ;; "\\(mld\\|ss\\)[A-Z]\\w+\\)\\|" - ;; "\\(new\\|open\\|close\\|save\\|find\\)_system\\|" - ;; "\\(add\\|delete\\|replace\\)_\\(block\\|line\\)\\|" - ;; "simulink\\|bd\\(root\\|close\\)" - ;; "\\)\\>") - 1 matlab-simulink-keyword-face) + 1 matlab-simulink-keyword-face) )) "Expressions to highlight in MATLAB mode.") +(defconst matlab-file-really-gaudy-font-lock-keywords + (append + matlab-file-gaudy-font-lock-keywords + matlab-really-gaudy-font-lock-keywords + ) + "Expressions to highlight in MATLAB mode.") + ;; Imenu support. (defvar matlab-imenu-generic-expression '((nil "^\\s-*function\\>[ \t\n.]*\\(\\(\\[[^]]*\\]\\|\\sw+\\)[ \t\n.]*\ @@ -1377,7 +1251,9 @@ indicates as such." (defun matlab-mode-leave () "When leaving `matlab-mode', turn off `mlint-minor-mode'" (when (eq major-mode 'matlab-mode) - (mlint-minor-mode -1))) + (mlint-minor-mode -1) + (matlab-scan-disable) + )) ;;;###autoload (define-derived-mode matlab-mode prog-mode "MATLAB" @@ -1389,15 +1265,10 @@ Convenient editing commands are: \\[matlab-fill-region] - Fill code and comments in region. \\[matlab-complete-symbol] - Symbol completion of matlab symbols\ based on the local syntax. - \\[matlab-indent-sexp] - Indent syntactic block of code. Convenient navigation commands are: \\[matlab-beginning-of-command] - Move to the beginning of a command. \\[matlab-end-of-command] - Move to the end of a command. - \\[matlab-beginning-of-defun] - Move to the beginning of a function. - \\[matlab-end-of-defun] - Move do the end of a function. - \\[matlab-forward-sexp] - Move forward over a syntactic block of code. - \\[matlab-backward-sexp] - Move backwards over a syntactic block of code. Convenient template insertion commands: \\[tempo-template-matlab-function] - Insert a function definition. @@ -1412,9 +1283,8 @@ with correctly quoted chars. Variables: `matlab-indent-level' Level to indent blocks. - `matlab-cont-level' Level to indent continuation lines. - `matlab-cont-requires-ellipsis' Does your MATLAB support implied elipsis. - `matlab-case-level' Level to unindent case statements. + `matlab-continuation-indent-level' Level to indent after ... continuation + `matlab-case-indent-level' Level to unindent case statements. `matlab-indent-past-arg1-functions' Regexp of functions to indent past the first argument on continuation lines. @@ -1423,12 +1293,9 @@ Variables: `fill-column' Column used in auto-fill. `matlab-indent-function-body' If non-nil, indents body of MATLAB functions. `matlab-functions-have-end' If non-nil, MATLAB functions terminate with end. - `matlab-return-function' Customize RET handling with this function. `matlab-fill-code' Non-nil, auto-fill code in auto-fill-mode. `matlab-fill-strings' Non-nil, auto-fill strings in auto-fill-mode. `matlab-verify-on-save-flag' Non-nil, enable code checks on save. - `matlab-highlight-block-match-flag' - Enable matching block begin/end keywords. `matlab-vers-on-startup' If t, show version on start-up. `matlab-handle-simulink' If t, enable simulink keyword highlighting. @@ -1443,55 +1310,63 @@ All Key Bindings: (if (boundp 'whitespace-modes) (add-to-list 'whitespace-modes 'matlab-mode)) (setq local-abbrev-table matlab-mode-abbrev-table) - (set-syntax-table matlab-mode-syntax-table) + + ;; Syntax tables and related features are in matlab-syntax.el + ;; This includes syntax table definitions, misc syntax regexps + ;; and font-lock for comments/strings. + (matlab-syntax-setup) + (matlab-scan-setup) + + ;; Indentation setup. (setq indent-tabs-mode nil) (make-local-variable 'indent-line-function) (setq indent-line-function 'matlab-indent-line) - (make-local-variable 'paragraph-start) - (setq paragraph-start (concat "^$\\|" page-delimiter)) - (make-local-variable 'paragraph-separate) - (setq paragraph-separate paragraph-start) - (make-local-variable 'paragraph-ignore-fill-prefix) - (setq paragraph-ignore-fill-prefix t) - (make-local-variable 'comment-start-skip) - (setq comment-start-skip "%\\s-+") - (make-local-variable 'comment-start) - (setq comment-start "%") - (make-local-variable 'page-delimiter) - (setq page-delimiter "^\\(\f\\|%%\\(\\s-\\|\n\\)\\)") + (make-local-variable 'indent-region-function) + (setq indent-region-function 'matlab-indent-region) (make-local-variable 'comment-column) (setq comment-column matlab-comment-column) (make-local-variable 'comment-indent-function) - (setq comment-indent-function 'matlab-comment-indent) + (setq comment-indent-function (lambda () nil)) ;; always use indent-according-to-mode + (make-local-variable 'electric-indent-functions) + (setq electric-indent-functions 'matlab-electric-indent-function) + + ;; Sexp's and Defuns + (make-local-variable 'forward-sexp-function) + (setq forward-sexp-function 'matlab-forward-sexp-fcn) + (make-local-variable 'beginning-of-defun-function) + (setq beginning-of-defun-function 'matlab-beginning-of-defun) + (make-local-variable 'end-of-defun-function) + (setq end-of-defun-function 'matlab-skip-over-defun) (make-local-variable 'add-log-current-defun-function) - (setq add-log-current-defun-function 'matlab-current-defun) - (make-local-variable 'fill-column) - (setq fill-column matlab-fill-column) - ;; Emacs 20 supports this variable. - ;; This lets users turn auto-fill on and off and still get the right - ;; fill function. + (setq add-log-current-defun-function 'matlab-add-log-current-defun) + + ;; Auto-Fill and Friends (make-local-variable 'normal-auto-fill-function) (setq normal-auto-fill-function 'matlab-auto-fill) + (make-local-variable 'fill-column) + (setq fill-column matlab-fill-column) (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) + ;; Save hook for verifying src. This lets us change the name of ;; the function in `write-file' and have the change be saved. ;; It also lets us fix mistakes before a `save-and-go'. (make-local-variable 'write-contents-functions) (add-hook 'write-contents-functions 'matlab-mode-verify-fix-file-fn) - ;; when a buffer changes, flush parsing data. - (add-hook 'after-change-functions 'matlab-change-function nil t) + ;; give each file it's own parameter history (make-local-variable 'matlab-shell-save-and-go-history) + + ;; Font lock support: (make-local-variable 'font-lock-defaults) - (setq font-lock-defaults '((matlab-font-lock-keywords - matlab-gaudy-font-lock-keywords - matlab-really-gaudy-font-lock-keywords + (setq font-lock-defaults '((matlab-file-font-lock-keywords + matlab-file-gaudy-font-lock-keywords + matlab-file-really-gaudy-font-lock-keywords ) - t ; do not do string/comment highlighting + nil ; use syntax table comments/strings nil ; keywords are case sensitive. ;; This puts _ as a word constituent, ;; simplifying our keywords significantly @@ -1499,28 +1374,16 @@ All Key Bindings: (setq font-lock-multiline 'undecided) (add-to-list 'font-lock-extend-region-functions #'matlab-font-lock-extend-region t) - ;; Parens mode support - (if (and (featurep 'paren) (symbolp 'show-paren-data-function) (symbolp show-paren-data-function)) - (progn - ;; show-paren-mode is nicer than our old thing. - (make-local-variable 'show-paren-data-function) - (setq show-paren-data-function #'matlab-show-paren-or-block) - ) - ;; Enable our own block highlighting if paren mode not around. - (matlab-enable-block-highlighting 1)) - - (if window-system (matlab-frame-init)) - - ;; built-in sexp navigation - (make-local-variable 'forward-sexp-function) - (setq forward-sexp-function #'matlab-move-simple-sexp-internal) - + ;; Highilght parens OR if/end type blocks + (make-local-variable 'show-paren-data-function) + (setq show-paren-data-function 'matlab-show-paren-or-block) + ;; If first function is terminated with an end statement, then functions have ;; ends. (if (matlab-do-functions-have-end-p) + ;; minor mode now treat's 'guess' as true when passing in 1. (matlab-functions-have-end-minor-mode 1) - (matlab-functions-have-end-minor-mode -1) - ) + (matlab-functions-have-end-minor-mode -1)) ;; When matlab-indent-function-body is set to 'MathWorks-Standard, ;; - we indent all functions that terminate with an end statement @@ -1537,38 +1400,14 @@ All Key Bindings: ) ((eq matlab-indent-function-body 'guess) - (save-excursion - (goto-char (point-max)) - - (if (re-search-backward matlab-defun-regex nil t) - (let ((beg (point)) - end ; filled in later - (cc (current-column)) - ) - (setq end (if matlab-functions-have-end - (progn (forward-line 0) (point)) - (point-max))) - (goto-char beg) - (catch 'done - (while (progn (forward-line 1) (< (point) end)) - (if (looking-at "\\s-*\\(%\\|$\\)") - nil ; go on to next line - (looking-at "\\s-*") - (goto-char (match-end 0)) - (setq matlab-indent-function-body (> (current-column) cc)) - (throw 'done nil)))) - ) - (setq matlab-indent-function-body 'MathWorks-Standard) - )) + (matlab-guess-function-indentation) ) - - (t) ) ;; When leaving matlab-mode, turn off mlint (add-hook 'change-major-mode-hook #'matlab-mode-leave) - (if matlab-vers-on-startup (matlab-show-version))) + ) (defun matlab-mode-init-mlint-if-needed () "Check if we should start `mlint-minor-mode' for this buffer." @@ -1590,6 +1429,26 @@ All Key Bindings: ;; continue. (error nil)))) + +;; Support debug mode and read only toggling. +(defvar gud-matlab-debug-active nil) +(declare-function matlab-shell-gud-minor-mode "matlab-shell-gud") + +(defun matlab-toggle-read-only (&optional arg interactive) + "Toggle read-only bit in MATLAB mode. +This looks to see if we are currently debugging, and if so re-enable +our debugging feature. +Optional argument ARG specifies if the read-only mode should be set. +INTERACTIVE is ignored." + (interactive "P") + (if (and (featurep 'matlab-shell-gud) + gud-matlab-debug-active) + ;; The debugging is active, just re-enable debugging read-only-mode + (matlab-shell-gud-minor-mode 1) + ;; Else - it is not - probably doing something else. + (call-interactively 'read-only-mode) + )) + ;;; Utilities ================================================================= @@ -1598,266 +1457,13 @@ All Key Bindings: (interactive) (message "matlab-mode, version %s" matlab-mode-version)) -(defun matlab-find-prev-line () - "Recurse backwards until a code line is found." - (if (= -1 (forward-line -1)) nil - (if (or (matlab-ltype-empty) - (matlab-ltype-comm-ignore)) - (matlab-find-prev-line) t))) +(defun matlab-find-code-line () + "Walk forwards until we are on a line of code return t on success. +If the currnet line is code, return immediately. +Ignore comments and whitespace." + (forward-comment 100000) + (not (eobp))) -(defun matlab-prev-line () - "Go to the previous line of code. Return nil if not found." - (interactive) - (let ((old-point (point))) - (if (matlab-find-prev-line) t (goto-char old-point) nil))) - -(defun matlab-uniquify-list (lst) - "Return a list that is a subset of LST where all elements are unique." - (let ((nlst nil)) - (while lst - (if (and (car lst) (not (member (car lst) nlst))) - (setq nlst (cons (car lst) nlst))) - (setq lst (cdr lst))) - (nreverse nlst))) - -; Aki Vehtari recommends this: (19.29 required) -;(require 'backquote) -;(defmacro matlab-navigation-syntax (&rest body) -; "Evaluate BODY with the matlab-mode-special-syntax-table" -; '(let ((oldsyntax (syntax-table))) -; (unwind-protect -; (progn -; (set-syntax-table matlab-mode-special-syntax-table) -; ,@body) -; (set-syntax-table oldsyntax)))) - -(defmacro matlab-navigation-syntax (&rest forms) - "Set the current environment for syntax-navigation and execute FORMS." - (list 'let '((oldsyntax (syntax-table)) - (case-fold-search nil)) - (list 'unwind-protect - (list 'progn - '(set-syntax-table matlab-mode-special-syntax-table) - (cons 'progn forms)) - '(set-syntax-table oldsyntax)))) - -(put 'matlab-navigation-syntax 'lisp-indent-function 0) -(add-hook 'edebug-setup-hook - (lambda () - (def-edebug-spec matlab-navigation-syntax def-body))) - -(defun matlab-up-list (count &optional restrict) - "Move forwards or backwards up a list by COUNT. -Optional argument RESTRICT is where to restrict the search." - ;; MATLAB syntax table has no disabling strings or comments. - (let ((dir (if (> 0 count) -1 +1)) - (origin (point)) - (ms nil)) - ;; Make count positive - (setq count (* count dir)) - (if (= dir -1) - (while (/= count 0) - ;; Search till we find an unstrung paren object. - (setq ms (re-search-backward "\\s(\\|\\s)" restrict t)) - (while (and (save-match-data (matlab-cursor-in-string-or-comment)) - (setq ms (re-search-backward "\\s(\\|\\s)" restrict t)))) - (if (not ms) - (progn - (goto-char origin) - (error "Scan Error: List mismatch"))) - ;; View it's match. - (let ((s (match-string 0))) - (if (string-match "\\s(" s) - (setq count (1- count)) - (setq count (1+ count))))) - (error "Not implemented")) - ms)) - -(defvar matlab-in-command-restriction nil - "Non-nil if currently in a `matlab-with-current-command' form.") - -(defmacro matlab-with-current-command (&rest forms) - "Restrict region to the current command and run FORMS. -Restore restriction after FORMS run. -This command will not add a restriction if we are already -restricted." - (declare (indent 0) (debug t)) - `(save-restriction - (when (not matlab-in-command-restriction) - (narrow-to-region (save-excursion - (matlab-beginning-of-command) - (beginning-of-line) - (point)) - (matlab-point-at-eol))) - (let ((matlab-in-command-restriction t)) - ,@forms - ))) - -(defun matlab-valid-end-construct-p () - "Return non-nil if the end after point terminates a block. -Return nil if it is being used to dereference an array." - (let ((p (point)) - (err1 t)) - (condition-case nil - (save-match-data - (matlab-with-current-command - ;; This used to add some sort of protection, but I don't know what - ;; the condition was, or why the simple case doesn't handle it. - ;; - ;; The above replacement fixes a case where a continuation in an array - ;; befuddles the identifier. - ;; (progn ;;(matlab-end-of-command (point)) - ;; (end-of-line) - ;; (if (> p (point)) - ;; (progn - ;; (setq err1 nil) - ;; (error))) - ;; (point)))) - (save-excursion - ;; beginning of param list - (matlab-up-list -1) - ;; backup over the parens. If that fails - (condition-case nil - (progn - (forward-sexp 1) - ;; If we get here, the END is inside parens, which is not a - ;; valid location for the END keyword. As such it is being - ;; used to dereference array parameters - nil) - ;; This error means that we have an unterminated paren - ;; block, so this end is currently invalid. - (error nil))))) - ;; an error means the list navigation failed, which also means we are - ;; at the top-level - (error err1)))) - -;;; Regexps for MATLAB language =============================================== - -;; "-pre" means "partial regular expression" -;; "-if" and "-no-if" means "[no] Indent Function" - -(defconst matlab-defun-regex "^\\(\\s-*function\\|classdef\\)[ \t.[]" - "Regular expression defining the beginning of a MATLAB function.") - -(defconst matlab-mcos-regexp "\\|classdef\\|properties\\|methods\\|events\\|enumeration\\|arguments" - "Keywords which mark the beginning of mcos blocks.") - -(defcustom matlab-block-indent-tic-toc-flag nil - "*Non-nil means that tic,toc should indent like a if,end block. -This variable should be set before loading matlab.el" - :group 'matlab - :type 'boolean) - -(defconst matlab-block-beg-pre-if - (if matlab-block-indent-tic-toc-flag - (concat "function\\|parfor\\|spmd\\|for\\|while\\|if\\|switch\\|try\\|tic" - matlab-mcos-regexp) - (concat "function\\|parfor\\|spmd\\|for\\|while\\|if\\|switch\\|try" - matlab-mcos-regexp)) - "Keywords which mark the beginning of an indented block. -Includes function.") - -(defconst matlab-block-beg-pre-no-if - (if matlab-block-indent-tic-toc-flag - (concat "parfor\\|for\\|spmd\\|while\\|if\\|switch\\|try\\|tic" - matlab-mcos-regexp) - (concat "parfor\\|for\\|spmd\\|while\\|if\\|switch\\|try" - matlab-mcos-regexp)) - "Keywords which mark the beginning of an indented block. -Excludes function.") - -(defun matlab-block-beg-pre () - "Partial regular expression to recognize MATLAB block-begin keywords." - (if matlab-functions-have-end - matlab-block-beg-pre-if - matlab-block-beg-pre-no-if)) - -(defconst matlab-block-mid-pre - "elseif\\|else\\|catch" - "Partial regular expression to recognize MATLAB mid-block keywords.") - -(defconst matlab-block-end-pre-if - (if matlab-block-indent-tic-toc-flag - "end\\|function\\|\\(\\sw+\\s-*\\((.*)\\)?\\s-*=\\s-*\\)?toc" - "end\\|function") - "Partial regular expression to recognize MATLAB block-end keywords.") - -(defconst matlab-block-end-pre-no-if - (if matlab-block-indent-tic-toc-flag - "end\\|\\(\\sw+\\s-*\\((.*)\\)?\\s-*=\\s-*\\)?toc" - "end") - "Partial regular expression to recognize MATLAB block-end keywords.") - -(defun matlab-block-end-pre () - "Partial regular expression to recognize MATLAB block-end keywords." - (if matlab-functions-have-end - matlab-block-end-pre-if - matlab-block-end-pre-no-if)) - -;; Not used. -;;(defconst matlab-other-pre -;; "function\\|return" -;; "Partial regular express to recognize MATLAB non-block keywords.") - -(defconst matlab-endless-blocks - "case\\|otherwise" - "Keywords which initialize new blocks, but don't have explicit ends. -Thus, they are endless. A new case or otherwise will end a previous -endless block, and end will end this block, plus any outside normal -blocks.") - -(defun matlab-block-re () - "Regular expression for keywords which begin MATLAB blocks." - (concat "\\(^\\|[;,]\\)\\s-*\\(" - (matlab-block-beg-pre) "\\|" - matlab-block-mid-pre "\\|" - (matlab-block-end-pre) "\\|" - matlab-endless-blocks "\\)\\b")) - -(defun matlab-block-scan-re () - "Expression used to scan over matching pairs of begin/ends." - (concat "\\(^\\|[;,]\\)\\s-*\\(" - (matlab-block-beg-pre) "\\|" - (matlab-block-end-pre) "\\)\\b")) - -(defun matlab-block-beg-re () - "Expression used to find the beginning of a block." - (concat "\\(" (matlab-block-beg-pre) "\\)")) - -(defun matlab-block-mid-re () - "Expression used to find block center parts (like else)." - (concat "\\(" matlab-block-mid-pre "\\)")) - -(defun matlab-block-end-re () - "Expression used to end a block. Usually just `end'." - (concat "\\(" (matlab-block-end-pre) "\\)")) - -(defun matlab-block-end-no-function-re () - "Expression representing and end if functions are excluded." - (concat "\\<\\(" matlab-block-end-pre-no-if "\\)\\>")) - -(defun matlab-endless-blocks-re () - "Expression of block starters that do not have associated ends." - (concat "\\(" matlab-endless-blocks "\\)")) - -(defun matlab-match-function-re () - "Expression to match a function start line. -There are no reliable numeric matches in this expression. -Know that `match-end' of 0 is the end of the function name." - ;; old function was too unstable. - ;;"\\(^function\\s-+\\)\\([^=\n]+=[ \t\n.]*\\)?\\(\\sw+\\)" - (concat "\\(^\\s-*function\\b[ \t\n.]*\\)\\(\\(\\[[^]]*\\]\\|\\sw+\\)" - "[ \t\n.]*=[ \t\n.]*\\|\\(\\)\\)\\(\\sw+\\)")) - -(defun matlab-match-classdef-re () - "Expression to match a classdef start line. -The class name is match 2." - "\\(^\\s-*classdef\\b[ \t\n]*\\)\\(\\sw+\\)\\(\\s-*<\\)?") - -(defconst matlab-cline-start-skip "[ \t]*%[ \t]*" - "*The regular expression for skipping comment start.") - - ;;; Navigation =============================================================== (defvar matlab-scan-on-screen-only nil @@ -1865,761 +1471,278 @@ The class name is match 2." This is so the block highlighter doesn't gobble up lots of time when a block is not terminated.") -(defun matlab-up-string-or-comment () - "If the cursor is in a string or comment, move cursor to end of that syntax. -Returns new location if the cursor is moved. nil otherwise." - (interactive) - (let* ((bounds nil) - (ctxt (matlab-cursor-comment-string-context 'bounds))) - (when ctxt - (goto-char (nth 1 bounds)) - (unless (eobp) - (when (eq ctxt 'comment) (forward-char 1))) - t))) - -(defun matlab-backward-up-string-or-comment () - "If the cursor is in a string or comment, move cursor to beginning of that syntax. -Returns new location if the cursor is moved. nil otherwise." - (interactive) - (let* ((bounds nil) - (ctxt (matlab-cursor-comment-string-context 'bounds))) - (when ctxt - (goto-char (nth 0 bounds)) - (unless (bobp) - (when (eq ctxt 'comment) (forward-char -1)) - (when (eq ctxt 'elipsis) (forward-char -3))) - t))) - -(defun matlab-move-list-sexp-internal (dir) - "Move over one MATLAB list sexp in direction DIR. -Only covers list sexp. If not adjacent to a list, do nothing." - (let ((depth 1) - (sc (if (> dir 0) #'skip-chars-forward #'skip-chars-backward)) - (fc (if (> dir 0) #'forward-char #'backward-char)) - (rx (if (> dir 0) #'re-search-forward #'re-search-backward)) - (start (point)) - (udir (if (> dir 0) 1 -1)) - (match nil)) - (funcall sc " \t\n") - (funcall fc 1) - (while (and (> depth 0) (funcall rx "\\s(\\|\\s)" nil t)) ; look up next paren thing - (let* ((bounds nil) - (ctxt (matlab-cursor-comment-string-context 'bounds))) - (if ctxt - ;; do nothing if found paren in a string or comment, but do move out of it. - (goto-char (if (< dir 0) (nth 0 bounds) (nth 1 bounds))) - - ;; We are in code somewhere, so decide if we have move into or out of our list. - (setq match (match-string 0)) - (cond ((string-match "\\s(" match) - (setq depth (+ depth udir)) - ) - ((string-match "\\s)" match) - (setq depth (- depth udir)) - ))))) - (when (> depth 0) - (goto-char (point)) - (error "Unbalanced Parenthesis")) - )) - -(defun matlab-move-simple-sexp-backward-internal (count) - "Move backward COUNT number of MATLAB sexps." - (interactive "P") - (unless count (setq count 1)) - (matlab-move-simple-sexp-internal (- count))) - -(defun matlab-move-simple-sexp-internal(count) - "Move over one MATLAB sexp COUNT times. -If COUNT is negative, travel backward." - (interactive "P") - (unless (eq count 0) - (unless count (setq count 1)) - ;; Get base whitespace out of the way first. - (if (> 0 count) - (skip-chars-backward " \t;.") - (skip-chars-forward " \t;.")) - - (let* ((bounds nil) - (ctxt (matlab-cursor-comment-string-context 'bounds)) - (skipnav nil)) - (when (and ctxt (not (eolp)) (not (looking-at "%"))) - ;; First, if we are IN a string or comment then navigate differently. - - ;; To start, if we are at the EDGE of the comment or string, skip - ;; over it so we can keep going. - (cond ((and (> 0 count) (< (- (point) (car bounds)) 3)) - ;; Skip out backward - (goto-char (car bounds))) - ((and (< 0 count) (< (- (nth 1 bounds) (point)) 2)) - ;; skip out forward - (goto-char (nth 1 bounds))) - (t - ;; Nav over regular words inside. - (save-restriction - (narrow-to-region (car bounds) (nth 1 bounds)) - (forward-word count)) - (when (eq (point) (car bounds)) - (forward-char (if (> 0 count) -1 1))) - (setq skipnav t) - ))) - - (unless skipnav - ;; Outside of comments and strings, look at our local syntax and decide what to do. - ;; Skip over whitespace to see what the next interesting thing is. - (while (not (= 0 count)) - (if (< 0 count) - (progn ;; forward motion - (skip-chars-forward " \t\n;.=") - - (cond ((or (looking-at "['\"]\\|%\\|\\.\\.\\.")) - ;; In a comment or string. - (forward-char 1) - (matlab-up-string-or-comment)) - - ((looking-at "\\s(") - ;; At the beginning of a list, matrix, cell, whatever. - (matlab-move-list-sexp-internal count)) - (t - (forward-symbol 1)) - ) - (setq count (1- count)) - ) - - ;; backward motion - (skip-chars-backward " \t\n;.=") - (let ((ctxt2 (matlab-cursor-comment-string-context))) - - (cond ((or (looking-back "['\"]" (- (point) 2)) - (and (eolp) (or (eq ctxt2 'comment) (eq ctxt2 'elipsis)))) - (backward-char 2) - (matlab-backward-up-string-or-comment)) - - ((looking-back "\\s)" (- (point) 1)) - ;; At the end of a list, matrix, cell, etc - (matlab-move-list-sexp-internal count)) - (t - (forward-symbol -1)) - )) - (setq count (1+ count)) - )) - )))) - (defun matlab-backward-sexp (&optional autoend noerror) "Go backwards one balanced set of MATLAB expressions. If optional AUTOEND, then pretend we are at an end. If optional NOERROR, then we return t on success, and nil on failure. This assumes that expressions do not cross \"function\" at the left margin." - (interactive "P") - (matlab-navigation-syntax - (skip-chars-backward " \t\n") - (cond - ;; Not auto-end, and on the end of a block comment - ((and (not autoend) - (matlab-cursor-in-comment) - (let ((bcend (save-excursion - (beginning-of-line) - (re-search-forward "%" (point-at-eol)) - (goto-char (match-beginning 0)) - (when (looking-at "%}") - (point))))) - (if bcend (goto-char bcend)))) + (let ((p (point)) + (returnme t) + keyword) + (save-excursion + (skip-syntax-backward " .><") + (cond + ;; Auto end - Just go! + (autoend + (when (matlab--scan-block-backward-up nil) + (if noerror + (setq returnme nil) + (error "Unstarted END"))) + ) - (let ((bc (matlab-ltype-block-comm))) - (goto-char (car bc))) - ) - ;; Not auto-end, and not looking @ and end type keyword - ((and (not autoend) - (save-excursion (backward-word 1) - (or (not - (and (looking-at - (matlab-block-end-no-function-re)) - (matlab-valid-end-construct-p))) - (matlab-cursor-in-string-or-comment)))) - ;; Go backwards one simple expression - (matlab-move-simple-sexp-internal -1)) + ;; No auto-end .... + + ;; End of a block comment + ((eq (matlab-line-comment-style (matlab-compute-line-context 1)) 'block-end) + (beginning-of-line) + (matlab-beginning-of-string-or-comment)) - ;; otherwise go backwards recursively across balanced expressions - ;; backup over our end - (t - (if (not autoend) (forward-word -1)) - (let ((done nil) (start (point)) (returnme t) (bound nil)) - (when (search-backward "\nfunction" nil t) - (if (progn (forward-char 9) (looking-at "\\b")) - (setq bound (- (point) 8))) - (goto-char start)) - (while (and (not done) - (or (not matlab-scan-on-screen-only) - (pos-visible-in-window-p))) - (if (re-search-backward (matlab-block-scan-re) bound t) - (progn - (goto-char (match-beginning 2)) - (if (looking-at (matlab-block-end-no-function-re)) - (if (or (matlab-cursor-in-string-or-comment) - (not (matlab-valid-end-construct-p))) - nil - ;; we must skip the expression and keep searching - (forward-word 1) - (unless (matlab-backward-sexp nil noerror) - (setq done t - returnme nil))) - (if (not (matlab-cursor-in-string-or-comment)) - (setq done t)))) - (goto-char start) - (if noerror - (setq done t - returnme nil) - (error "Unstarted END construct")))) - returnme))))) + ((or (not (setq keyword (matlab-on-keyword-p))) + (memq keyword '(decl ctrl mcos arg vardecl keyword))) + ;; Just walk over block starts and other random stuff. + (matlab-move-simple-sexp-internal -1)) -(defun matlab-forward-sexp (&optional includeelse autostart) + ((memq keyword '(mid case)) + ;; If we're on a middle, then assume we're in the middle + ;; of something and keep going. + (when (matlab--scan-block-backward-up nil) + (if noerror + (setq returnme nil) + (error "Unstarted END"))) + ) + + (t + (when (matlab--scan-block-backward nil) + (if noerror + (setq returnme nil) + (error "Unstarted END"))) + ) + ) + + (when returnme + (setq p (point)))) + (goto-char p) + returnme)) + +(defun matlab-forward-sexp-fcn (&optional arg) + "Function used as `forward-sexp-function' for MATLAB mode. +Adapt to use `matlab-forward-sexp' or `matlab-backward-sexp' +depending on value of 'arg'." + ;; Move forward on positive arg. + (while (> arg 0) + (matlab-forward-sexp) + (setq arg (1- arg))) + ;; Or maybe move backward on negative args. + (while (< arg 0) + (matlab-backward-sexp) + (setq arg (1+ arg))) + ) + + +(defun matlab-forward-sexp (&optional autostart parentblock) "Go forward one balanced set of MATLAB expressions. -Optional argument INCLUDEELSE will stop on ELSE if it matches the starting IF. If AUTOSTART is non-nil, assume we are already inside a block, and navigate -forward until we exit that block." - (interactive "P") - (let (p) ;; go to here if no error. - (save-excursion ;; don't go anywhere if there is an error - (matlab-navigation-syntax - ;; skip over preceding whitespace - (skip-chars-forward " \t\n;") - (cond - ;; no autostart, and looking at a block comment. - ((and (not autostart) - (matlab-ltype-block-comment-start)) - (goto-char (match-end 0)) - (let ((bc (matlab-ltype-block-comm))) - (when bc (goto-char (cdr bc)))) - ) - ;; No autostart, and looking at a block keyword. - ((and (not autostart) - (or (not (looking-at (concat "\\(" - (matlab-block-beg-pre) - "\\|" - (matlab-block-mid-re) - "\\)\\>"))) - (matlab-cursor-in-string-or-comment))) - ;; Go forwards one simple expression - (matlab-move-simple-sexp-internal 1)) +forward until we exit that block. +PARENTBLOCK is used when recursing to validate block starts as being in +a valid context." + (let (p keyword) ;; go to here if no error. + (save-excursion ;; Don't move if there is an error + ;; skip over preceding whitespace + (skip-syntax-forward " .><") + (cond + ;; Auto start - just go! + (autostart + (when (matlab--scan-block-forward-up nil) + (error "Unterminated Block") + )) - ;; Yes autostart, but already looking @ the END! - ((and autostart (looking-at (matlab-block-end-re))) - (goto-char (match-end 0))) + ;; No Autostart .... - ;; Default behavior. - (t - ;; Not autostart, skip next word. - (unless autostart (forward-word 1)) - (let ((done nil) (s nil) - (expr-scan (if includeelse - (matlab-block-re) - (matlab-block-scan-re))) - (expr-look (matlab-block-beg-pre))) - (while (and (not done) - (setq s (re-search-forward expr-scan nil t)) - (or (not matlab-scan-on-screen-only) - (pos-visible-in-window-p))) - (goto-char (match-beginning 2)) - (if (looking-at expr-look) - (if (matlab-cursor-in-string-or-comment) - (forward-word 1) - ;; we must skip the expression and keep searching - ;; NEVER EVER call with value of INCLUDEELSE - (matlab-forward-sexp)) - (forward-word 1) - (if (and (not (matlab-cursor-in-string-or-comment)) - (matlab-valid-end-construct-p)) - (setq done t)))) - (if (not s) - (error "Unterminated block"))))) - (setq p (point)))) ;; really go here + ;; Looking at a block comment. + ((and (not autostart) + (looking-at "%")) + (goto-char (match-end 0)) + (matlab-end-of-string-or-comment)) + + ((or (not (setq keyword (matlab-on-keyword-p))) + (memq keyword '(end vardecl keyword))) + ;; Just walk over ends and other random stuff. + (matlab-move-simple-sexp-internal 1)) + + ((memq keyword '(mid case)) + ;; If we're on a middle, then assume we're in the middle + ;; of something and keep going. + (when (matlab--scan-block-forward-up nil) + (error "Unterminated Block")) + ) + + (t + (when (matlab--scan-block-forward nil nil) + (error "Unterminated Block")) + ) + ) + (setq p (point))) (goto-char p))) -(defun matlab-indent-sexp () - "Indent the syntactic block starting at point." - (interactive) - (indent-region (point) (save-excursion (matlab-forward-sexp) (point)) nil)) +(defun matlab-beginning-of-defun (&optional arg) + "Go to the beginning of the current function. +With optional ARG, go backward that many defuns." + (interactive "p") + (unless arg (setq arg 1)) + (let ((ans nil)) + ;; If ARG is positive, move BACKWARD that many defuns. + (while (> arg 0) + (setq ans (matlab--beginning-of-defun-raw)) + (setq arg (1- arg))) + ;; If ARG is negative, move FORWARD that many defun + (while (< arg 0) + (if (eq (matlab-on-keyword-p) 'decl) + (setq ans (not (matlab--scan-block-forward))) + ;; Else, just look for stuff and hope for the best. + (setq ans (matlab--scan-next-keyword 'decl (point-max))) + ) + (setq arg (1+ arg))) + + ans)) -(defun matlab-beginning-of-enclosing-defun () - "Move cursor to beginning of enclosing function. -If `matlab-functions-have-end', skip over functions with end." - (catch 'done - (let ((start (point)) - (beg nil)) - (while (re-search-backward matlab-defun-regex nil t) - (setq beg (point)) - (condition-case nil - (progn - (matlab-forward-sexp) - (if (> (point) start) (throw 'done beg))) - (error (throw 'done beg))) - (goto-char beg))) - nil)) +(defun matlab-skip-over-defun () + "Assigned to `end-of-defun-function' for matlab mode. +Assume point is on a defun, and if so, skip to the end." + (skip-syntax-forward " >") + (if (eq (matlab-on-keyword-p) 'decl) + (matlab--scan-block-forward) + ;; Else, bad condition. Maybe we're moving up from + ;; inside a nested function? If so, bounce up + ;; and try again. + (matlab-end-of-defun 1))) -(defun matlab-beginning-of-defun () - "Go to the beginning of the current function." - (interactive) - (if matlab-functions-have-end - (goto-char (or (matlab-beginning-of-enclosing-defun) (point-min))) - (or (re-search-backward matlab-defun-regex nil t) - (goto-char (point-min))))) -(defun matlab-end-of-defun () +(defun matlab-end-of-defun (&optional arg) "Go to the end of the current function." - (interactive) - (or (progn - (if (looking-at matlab-defun-regex) (goto-char (match-end 0))) - (if (re-search-forward matlab-defun-regex nil t) - (progn (forward-line -1) - t))) - (goto-char (point-max)))) + (interactive "p") + (unless arg (setq arg 1)) + (let ((ans nil)) + (while (> arg 0) + (matlab-end-of-string-or-comment t) + (skip-syntax-forward " ") + (when (not (eq (matlab-on-keyword-p) 'decl)) + (matlab--scan-block-backward-up-until 'decl)) + (skip-syntax-forward " ") + (setq ans + (if (eq (matlab-on-keyword-p) 'decl) + (not (matlab--scan-block-forward)) + nil)) + (setq arg (1- arg))) + ans)) + +(defun matlab--beginning-of-defun-raw () + "Move to the beginning of defun cursor is in. +Move up and backwards one defun, our out of current defun. +Accounts for nested functions." + ;; Get out of comments. + (matlab-beginning-of-string-or-comment t) + ;; back over whitespace - try to find what we are near. + (skip-syntax-backward " >") + ;; Do scanning + (if (not (eq (matlab-on-keyword-p) 'end)) + ;; No end, scan up until we find the declaration we're in. + (matlab--scan-block-backward-up-until 'decl) + ;; Else, nav backward over the end we are at. + (matlab--scan-block-backward) + (if (eq (matlab-on-keyword-p) 'decl) + t ; done + ;; If that end wasn't a decl, scan upward. + (matlab--scan-block-backward-up-until 'decl)))) + +(defun matlab-add-log-current-defun () + "Return a text string represneting the current block. +Tries to return the current defun. If not, look for a +cell block with a name." + (or (matlab-current-defun) (matlab-current-cell))) + +(defun matlab-current-cell () + "Return the name of the current cell. +The name is any text after the %% and any whitespace." + (save-excursion + (forward-page -1) + (let ((lvl1 (matlab-compute-line-context 1)) + start) + (when (and (matlab-line-comment-p lvl1) + (eq (matlab-line-comment-style lvl1) 'cell-start)) + ;; We are in a cell start, get the content + (goto-char (matlab-line-point lvl1)) + (skip-chars-forward "% \t.,*" (point-at-eol)) + (setq start (point)) + (end-of-line 1) + (skip-chars-backward " \t*" start) + (buffer-substring-no-properties start (point)) + )) + )) (defun matlab-current-defun () "Return the name of the current function." (save-excursion - (matlab-beginning-of-defun) - (if (looking-at (matlab-match-function-re)) - (progn - (goto-char (match-end 0)) - (current-word))))) + (matlab--beginning-of-defun-raw) + (nth 1 (matlab-line-declaration-name)))) + (defun matlab-beginning-of-command () "Go to the beginning of an M command. Travels across continuations." - (interactive) - (beginning-of-line) - (let ((p nil) - ;; This restriction is a wild guess where to end reverse - ;; searching for array continuations. The reason is that - ;; matlab up list is very slow, and most people would never - ;; put a blank line in a matrix. Either way, it's worth the - ;; trade off to speed this up for large files. - ;; This list of keywords is NOT meant to be comprehensive. - (r (save-excursion - (re-search-backward - "^\\s-*\\(%\\|if\\|else\\(if\\)\\|while\\|\\(par\\)?for\\|$\\)\\>" - nil t))) - (bc (matlab-ltype-block-comm))) - (if bc - ;; block comment - just go to the beginning. - (goto-char (car bc)) + (interactive "P") + (matlab-scan-beginning-of-command)) - ;; Scan across lines that are related. - (while (and (or (matlab-prev-line-cont) - (matlab-ltype-continued-comm) - (setq p (matlab-lattr-array-cont r))) - (save-excursion (beginning-of-line) (not (bobp)))) - (if p (goto-char p) (matlab-prev-line)) - (setq p nil))) - (back-to-indentation))) - -(defun matlab-end-of-command (&optional beginning) +(defun matlab-end-of-command () "Go to the end of an M command. -Optional BEGINNING is where the command starts from." +Travells a cross continuations" (interactive) - (while (and (or (matlab-lattr-cont) - (save-excursion - (forward-line 1) - (or (matlab-ltype-continued-comm) - (matlab-lattr-array-cont beginning)))) - ;; This hack is a short circuit. If a user did not - ;; correctly end a matrix, this will short-circuit - ;; as soon as something that would never appear in a matrix - ;; becomes visible. - (not (save-excursion - (beginning-of-line) - (and (looking-at (matlab-block-scan-re)) - (not (looking-at (matlab-match-function-re)))))) - ;; If we hit the end of the buffer unexpectedly, this test - ;; will fail and we'll avoid looping forever. (E.g., this - ;; is triggered if a continuation line is the last one in - ;; the buffer, and the line lacks the final newline.) - (zerop (forward-line 1)))) - (end-of-line)) + (matlab-scan-end-of-command)) -;;; Line types, attributes, and string/comment context ================================================= -(defvar matlab-ltype-block-comm-bounds nil - "Bounds of the last block comment detected. -The bounds returned in this form: - (START END)") -(defvar matlab-ltype-block-comm-lastcompute nil - "Location of the last computation for block comments.") +;;; Line types, attributes, and string/comment context ==================================== - -(defun matlab-change-function (beg end length) - "Function run after a buffer is modified. -BEG, END, and LENGTH are unused." - ;; Flush block comment parsing info since those - ;; locations change on buffer edit. - (setq matlab-ltype-block-comm-bounds nil - matlab-ltype-block-comm-lastcompute nil) - ) - -(defun matlab-ltype-empty () ; blank line - "Return t if current line is empty." - (save-excursion - (beginning-of-line) - (looking-at "^[ \t]*$"))) - -(defun matlab-ltype-comm-noblock () - "Return t if the current line is a MATLAB single-line comment. -Returns nil for Cell start %% and block comments %{, %}. -Used in `matlab-ltype-comm', but specialized for not cell start." - (save-excursion - (beginning-of-line) - (looking-at "[ \t]*%\\([^%{}]\\|$\\)"))) - -(defun matlab-ltype-comm () ; comment line - "Return t if current line is a MATLAB comment line. -Return the symbol 'cellstart if it is a double %%. -Return the symbol 'blockcomm if we are in a block comment." - (save-excursion - (beginning-of-line) - (cond - ((matlab-ltype-block-comm) - 'blockcomm) - ((matlab-ltype-comm-noblock) - t) - ((looking-at "[ \t]*%%") - 'cellstart) - (t nil)))) - -(defun matlab-ltype-comm-ignore () ; comment out a region line - "Return t if current line is a MATLAB comment region line." - (save-excursion - (beginning-of-line) - (looking-at (concat "[ \t]*" matlab-comment-region-s)))) - -(defun matlab-ltype-help-comm () - "Return position of function decl if the point in a MATLAB help comment." - (save-excursion - (if (not (matlab-ltype-comm-noblock)) - nil - (save-restriction - (widen) - (while (and (matlab-ltype-comm-noblock) (not (bobp)) - (matlab-prev-line)) - (beginning-of-line)) - (when (matlab-ltype-function-definition) - (point)))))) - -;; (global-set-key [f6] 'matlab-debug-block-comm) -(defun matlab-debug-block-comm () - "Test block comment detector since font-lock won't let us debug." - (interactive) - (let ((pos (matlab-ltype-block-comm t))) - (if pos - (pulse-momentary-highlight-region (car pos) (cdr pos)) - (message "No block comment.")))) - -(defun matlab-ltype-block-comm (&optional linebounds) - "Return start positions of block comment if we are in a block comment. -Optional LINEBOUNDS specifies if returned limits are line based instead -of character based." - (let ((bounds matlab-ltype-block-comm-bounds) - (lcbounds matlab-ltype-block-comm-lastcompute)) - - ;;(if bounds (message "Recycle bounds") (message "no recycle bounds")) - - (cond ((and bounds (>= (point) (car bounds)) - (<= (point) (cdr bounds))) - ;; All set! - nil) - - ((and lcbounds (>= (point) (car lcbounds)) - (<= (point) (cdr lcbounds))) - ;; Also all set - nil) - - (t - (setq bounds (matlab-ltype-block-comm-1))) - ) - - (if (not bounds) - - ;; cache point location - (setq matlab-ltype-block-comm-lastcompute - (cons (point-at-bol) (point-at-eol))) - - ;; Else clear lastcompute location - (setq matlab-ltype-block-comm-lastcompute nil) - ;; Check if caller wants line bounds. - (when (and linebounds bounds) - (save-excursion - (goto-char (car bounds)) - (setcar bounds (point-at-bol)) - (goto-char (cdr bounds)) - (setcdr bounds (point-at-eol))))) - - bounds)) - -(defun matlab-ltype-block-comm-1 () - "Return the start positions of block comment if we are in a block comment." - (save-match-data - (save-excursion - (let ((start nil) - (good t) - (end nil)) - (if (and (matlab-ltype-block-comment-start) - (not (matlab-cursor-in-string-or-comment))) - (setq start (match-beginning 0)) ;; CHECK - - (while (and (setq good (re-search-backward "^\\s-*\\%\\([{}]\\)\\s-*$" nil t)) - (matlab-cursor-in-string-or-comment)) - nil) - - (when (and good (matlab-ltype-block-comment-start)) - (setq start (match-beginning 0)))) - - (when start - (while (and (setq good (re-search-forward matlab-block-comment-end-re nil t)) - (matlab-cursor-in-string t)) - nil) - - (if (and good (goto-char (match-beginning 0)) (matlab-ltype-block-comment-end)) - (setq end (match-end 0)) - (setq end (point-max)))) - - (if (and start end) - (setq matlab-ltype-block-comm-bounds (cons start end)) - (setq matlab-ltype-block-comm-bounds nil)))))) - -(defun matlab-ltype-block-comm-at-start () - "Return non-nil if we are on a block comment start line AND -the %{ is the only non-whitespace text on the line." - (matlab-ltype-block-comment-start)) - -(defun matlab-ltype-block-comm-at-end () - "Return non-nil if we are on a block comment end line AND -the %{ is the only non-whitespace text on this line." - (matlab-ltype-block-comment-end)) - - -(defun matlab-ltype-continued-comm () +(defun matlab-line-continued-comment () "Return column of previous line's comment start, or nil." (save-excursion (beginning-of-line) - (let ((commtype (matlab-ltype-comm-noblock))) - (if (or (null commtype) + (let* ((lvl (matlab-compute-line-context 1))) + (if (or (null (matlab-line-regular-comment-p lvl)) (bobp)) nil - ;; We use forward-line and not matlab-prev-line because - ;; we want blank lines to terminate this indentation method. + ;; We use forward-line -1 and not matlab-previous-command-begin + ;; because we want blank lines to terminate this indentation method. (forward-line -1) - (let ((col (matlab-lattr-comm))) - (if col - (progn - (goto-char col) - (current-column)) - nil)))))) + (matlab-line-end-comment-column (matlab-compute-line-context 1)))))) -(defun matlab-ltype-function-definition () - "Return t if the current line is a function definition." - (save-excursion - (beginning-of-line) - (looking-at matlab-defun-regex))) +(defun matlab-line-count-block-change (&optional lvl1-start lvl1-end) + "Count the change in block depth across lines. +Start at LVL1-START, and end at LVL1-END. It is ok if these +are the same line. +A positive number means there were more block starts and ends. +A negative number means there were more ends than starts. +0 means no blocks, or that all blocks started also ended." + ;; TODO - delete this + (unless lvl1-start (setq lvl1-start (matlab-compute-line-context 1) + lvl1-end lvl1-start)) -(defun matlab-ltype-code () ; line of code - "Return t if current line is a MATLAB code line." - (and (not (matlab-ltype-empty)) (not (matlab-ltype-comm)))) + (if (or (matlab-line-comment-p lvl1-start) + (matlab-line-empty-p lvl1-start)) + ;; If we are starting on comments or empty, no code to scan + 0 + (matlab-with-context-line lvl1-start + (let ((depth 0) + (bounds (matlab-line-end-of-code lvl1-end)) + (keyword nil)) + ;; Lets scan for keywords. + (goto-char (matlab-line-point lvl1-start)) + (while (setq keyword (matlab--scan-next-keyword 'blocks bounds)) + (cond ((eq (car keyword) 'end) + (setq depth (1- depth))) + (t + (setq depth (1+ depth))))) + + depth)))) -(defun matlab-lattr-comm () ; line has comment - "Return t if current line contain a comment." - (save-excursion (matlab-comment-on-line))) - -(defun matlab-lattr-implied-continuation () - "Return non-nil if this line has implied continuation on the next. -This is only useful for new versions of MATLAB where ... is optional." - (when (not (matlab-lattr-comm)) - (let ((imp nil)) - (save-excursion - (end-of-line) - (skip-chars-backward " \t") - ;; Test for operator incompleteness. - (setq imp - (/= (point) - ;; Careful, - means range in this expression. - (progn (skip-chars-backward "-+=/*.^&~<>") - (point)))) - (if (not imp) - ;; Test for argument list incompleteness - (condition-case nil - (progn - (end-of-line) - (matlab-up-list -1) - (setq imp (looking-at "("))) - (error nil))) - ) - imp))) - -(defun matlab-lattr-cont () ; line has continuation - "Return non-nil if current line ends in ... and optional comment. -If `matlab-cont-requires-ellipsis' is nil, then we need to apply -a heuristic to determine if this line would use continuation -based on what it ends with." - (save-excursion - (beginning-of-line) - (or - ;; Here, if the line ends in ..., then it is what we are supposed to do. - (and (re-search-forward "[^ \t.][ \t]*\\(\\.\\.\\.+\\)[ \t]*\\(.*\\)?$" - (matlab-point-at-eol) t) - (progn (goto-char (match-beginning 1)) - (not (matlab-cursor-in-string-or-comment)))) - ;; If the line doesn't end in ..., but we have optional ..., then - ;; use this annoying heuristic. - (and (null matlab-cont-requires-ellipsis) - (matlab-lattr-implied-continuation)) - ))) - -(defun matlab-prev-line-cont () - "Return t if the previous line is a continuation line." - (save-excursion (and (if (= -1 (forward-line -1)) nil t) - (matlab-lattr-cont)))) - -(defun matlab-lattr-array-cont (&optional restrict) - "Return non-nil if current line is in an array. -If the entirety of the array is on this line, return nil. -Optional option RESTRICT is the distance to restrict the search." - (condition-case nil - (save-excursion - (beginning-of-line) - (matlab-up-list -1 restrict) - (and (looking-at "[[{]") (point))) - (error nil))) - -(defun matlab-lattr-array-end () - "Return non-nil if the current line closes an array. -by close, the first character is the end of an array." - (save-excursion - (back-to-indentation) - (and (looking-at "[]}]") (matlab-lattr-array-cont)))) - -(defun matlab-lattr-block-cont (&optional eol) - "Return a number representing the number of unterminated block constructs. -This is any block, such as if or for, that doesn't have an END on this line. -Optional EOL indicates a virtual end of line." - (let ((v 0)) - (save-excursion - (beginning-of-line) - (save-restriction - (narrow-to-region (point) (or eol (matlab-point-at-eol))) - (matlab-navigation-syntax - (while (re-search-forward (concat "\\<" (matlab-block-beg-re) "\\>") - nil t) - (if (matlab-cursor-in-string-or-comment) - ;; Do nothing - nil - ;; Increment counter, move to end. - (setq v (1+ v)) - (let ((p (point))) - (forward-word -1) - (condition-case nil - (progn - (matlab-forward-sexp) - (setq v (1- v))) - (error (goto-char p)))))) - v))))) - -(defun matlab-lattr-middle-block-cont () - "Return the number of middle block continuations. -This should be 1 or nil, and only true if the line starts with one of these -special items." - (save-excursion - (back-to-indentation) - (if (looking-at (concat (matlab-block-mid-re) "\\>")) - (if (and (re-search-forward (matlab-block-end-pre) - (matlab-point-at-eol) - t) - (matlab-valid-end-construct-p)) - ;; If there is an END, we still need to return non-nil, - ;; but the number value is a net of 0. - 0 - 1) - nil))) - -(defun matlab-lattr-endless-block-cont () - "Return the number of middle block continuations. -This should be 1 or nil, and only true if the line starts with one of these -special items." - (save-excursion - (back-to-indentation) - (if (looking-at (concat (matlab-endless-blocks-re) "\\>")) - 1 - nil))) - -(defun matlab-lattr-block-close (&optional start) - "Return the number of closing block constructs. -Argument START is where to start searching from." - (let ((v 0)) - (save-excursion - (when start (goto-char start)) - (matlab-with-current-command - (goto-char (point-max)) - - ;; If in a comment, move out of it first. - (matlab-backward-up-string-or-comment) - - ;; Count every END in the line, skipping over active blocks - (while (re-search-backward (concat "\\<" (matlab-block-end-re) "\\>") - nil t) - (let ((startmove (match-end 0)) - (nomove (point))) - (cond - ((matlab-backward-up-string-or-comment) - ;; Above returns t if it was in a string or comment. - ;; In that case, we need to keep going. - nil) - ((not (matlab-valid-end-construct-p)) - ;; Not a valid end, just move past it. - (goto-char nomove)) - (t - ;; Lets count these end constructs. - (setq v (1+ v)) - (if (matlab-backward-sexp t t) - (setq v (1- v)) - (goto-char nomove))) - ))) - ;; If we can't scoot back, do a cheat-test to see if there - ;; is a matching else or elseif. - (goto-char (point-min)) - (back-to-indentation) - (if (looking-at (matlab-block-mid-re)) - (setq v (1- v))) - ;; Return nil, or a number - (if (<= v 0) nil v))))) - -(defun matlab-lattr-local-end () - "Return t if this line begins with an end construct." - (save-excursion - (back-to-indentation) - (and (looking-at (concat "\\<" (matlab-block-end-re) "\\>")) - (matlab-valid-end-construct-p)))) - -(declare-function matlab-property-function "matlab-complete" ()) -(defun matlab-lattr-semantics (&optional prefix) - "Return the semantics of the current position. -Values are nil 'solo, 'value, and 'boolean. Boolean is a subset of -value. nil means there is no semantic content (ie, string or comment.) -If optional PREFIX, then return 'solo if that is the only thing on the -line." - (cond ;((matlab-cursor-in-string-or-comment) - ;nil) - ((or (matlab-ltype-empty) - (and prefix (save-excursion - (beginning-of-line) - (looking-at (concat "\\s-*" prefix "\\s-*$"))))) - 'solo) - ((save-excursion - (matlab-beginning-of-command) - (looking-at "\\s-*\\(if\\|elseif\\|while\\)\\>")) - 'boolean) - ((save-excursion - (matlab-beginning-of-command) - (looking-at (concat "\\s-*\\(" (matlab-property-function) - "\\)\\>"))) - 'property) - (t - 'value))) (defun matlab-function-called-at-point () "Return a string representing the function called nearby point." @@ -2632,602 +1755,501 @@ line." (match-string 1)) (t nil)))) -(defun matlab-show-cursor-context () - "Display something about the context the cursor is in." - (interactive) - (let* ((bounds nil) - (ctxt (matlab-cursor-comment-string-context 'bounds))) - (if (not ctxt) - (message "Cursor not in a comment or string.") - - (message "Ctxt: %s Bounds: %S" ctxt bounds) - (when (featurep 'pulse) - (pulse-momentary-highlight-region (car bounds) (car (cdr bounds))) - )))) - - -(defun matlab-cursor-comment-string-context (&optional bounds-sym) - "Return the comment/string context of cursor for the current line. -Return 'comment if in a comment. -Return 'string if in a string. -Return 'charvector if in a character vector -Return 'elipsis if after an ... elipsis -Return nil if none of the above. -Scans from the beginning of line to determine the context. -If optional BOUNDS-SYM is specified, set that symbol value to the -bounds of the string or comment the cursor is in" - (save-match-data - (save-restriction - (narrow-to-region (matlab-point-at-bol) (matlab-point-at-eol)) - (let ((p (point)) - (returnme nil) - (sregex (concat "\\(%\\)\\|" matlab-string-start-regexp "\\('\\|\"\\)\\|\\(\\.\\.\\.\\)")) - (insregex "\\('\\|\"\\)") - (laststart nil)) - (save-excursion - (goto-char (point-min)) - (while (and (not (eq returnme 'comment)) - (not (eq returnme 'elipsis)) - (re-search-forward (if (not returnme) sregex insregex) nil t) - (<= (point) p)) - (cond ((eq returnme nil) - (cond ((= (preceding-char) ?%) - (setq returnme 'comment)) - ((= (preceding-char) ?.) - (setq returnme 'elipsis)) - ((= (preceding-char) ?') - (setq returnme 'charvector)) - ((= (preceding-char) ?\") - (setq returnme 'string))) - (setq laststart (1- (point))) ; store location - ) - ;; ((eq returnme comment) % not possible) - ((eq returnme 'string) - (cond ((= (preceding-char) ?%) nil) ; ok - ((= (preceding-char) ?') nil) ; ok - ((= (preceding-char) ?\") - (if (looking-at "\"") - (forward-char 1) ; skip quoted quote - (setq returnme nil ; end of string - laststart nil) - )))) - ((eq returnme 'charvector) - (cond ((= (preceding-char) ?%) nil) ; ok - ((= (preceding-char) ?\") nil) ; ok - ((= (preceding-char) ?') - (if (looking-at "'") - (forward-char 1) ; skip quoted quote - (setq returnme nil ; end of charvec - laststart nil)) - ))) - (t (message "Bug in `matlab-cursor-comment-string-context'"))))) - - ;; If we want to get the bounds of the string or comment the cursor is in - ;; then we need to find the end of whatever it is. - (when (and bounds-sym laststart) - (if (or (eq returnme 'comment) (eq returnme 'elipsis)) - ;; Comments and elipsis always end at end of line. - (set bounds-sym (list laststart (point-at-eol))) - - ;; Strings/charvec we need to keep searching forward. - (save-excursion - (let ((done nil) - (searchlim (point-at-eol)) - (strchar (cond ((eq returnme 'charvector) ?') - ((eq returnme 'string) ?\") - (t (error "Bug in matlab-cursor-comment-string-context"))))) - (save-match-data - (while (and (not done) - (re-search-forward "['\"]" searchlim t)) - (goto-char (match-end 0)) - (if (eq (preceding-char) strchar) - ;; Same type of string - (if (eq (following-char) strchar) - ;; This is a quoted quote. Skip it and keep going. - (forward-char 1) - ;; solo quote, end of string - (set bounds-sym (list laststart (point))) - (setq done t)) - ;; The other type of string - just keep going. - nil))) - (when (not done) - (set bounds-sym (list laststart (point-at-eol)))) - )))) - - ;; Return the identified context. - returnme)))) - -(defun matlab-cursor-in-string-or-comment () - "Return t if the cursor is in a valid MATLAB comment or string." - ;; comment and string depend on each other. Here is one test - ;; that does both. - (let ((ctxt (matlab-cursor-comment-string-context))) - (if (not ctxt) - nil - t))) - -(defun matlab-cursor-in-comment () - "Return t if the cursor is in a valid MATLAB comment." - (let ((ctxt (matlab-cursor-comment-string-context))) - (eq ctxt 'comment))) - -(defun matlab-cursor-in-string (&optional incomplete) - "Return t if the cursor is in a valid MATLAB character vector or string scalar. -Note: INCOMPLETE is now obsolete -If the optional argument INCOMPLETE is non-nil, then return t if we -are in what could be a an incomplete string. (Note: this is also the default)" - (let ((ctxt (matlab-cursor-comment-string-context))) - (or (eq ctxt 'string) (eq ctxt 'charvector)))) (defun matlab-comment-on-line () "Place the cursor on the beginning of a valid comment on this line. If there isn't one, then return nil, point otherwise." (interactive) - (let ((eol (matlab-point-at-eol)) - (p (point)) - (signal-error-on-buffer-boundary nil)) - (beginning-of-line) - (while (and (re-search-forward "%" eol t) - (save-excursion (forward-char -1) (matlab-cursor-in-string t)))) - (if (not (bolp)) (forward-char -1)) - (if (and (looking-at "%") (not (matlab-cursor-in-string t))) - (point) - (goto-char p) + (let* ((lvl1 (matlab-compute-line-context 1)) + (comm (matlab-line-end-comment-point lvl1))) + (if comm + (goto-char comm) nil))) ;;; Indent functions ========================================================== +;; + +(defun matlab-indent-region (start end &optional column noprogress) + "Indent the region between START And END for MATLAB mode. +Unlike `indent-region-line-by-line', this function captures +parsing state and re-uses that state along the way." + (interactive) + (save-excursion + (setq end (copy-marker end)) + (goto-char start) + (let ((pr (when (and (not (minibufferp)) (not noprogress)) + (make-progress-reporter "MATLAB Indenting region..." (point) end))) + (lvl2 nil) + (lvl1 nil) + ) + (while (< (point) end) + (unless (and (bolp) (eolp)) + ;; This is where we indent each line + (setq lvl1 (matlab-compute-line-context 1) + lvl2 (matlab-compute-line-context 2 lvl1)) ;; lvl2)) + (when (matlab--indent-line lvl2) + ;; If the indent changed something, refresh this + ;; context obj. + ;;(matlab-refresh-line-context-lvl2 lvl2) + )) + (forward-line 1) + (and pr (progress-reporter-update pr (point)))) + (and pr (progress-reporter-done pr)) + (move-marker end nil)))) + (defun matlab-indent-line () "Indent a line in `matlab-mode'." (interactive) - (let ((i (matlab-calc-indent)) - (ci (current-indentation)) - (cc (current-column))) + (let ((lvl2 (matlab-compute-line-context 2))) + (matlab--indent-line lvl2))) + +(defvar matlab--change-indentation-override #'matlab--change-indentation + "Tests to override this to validate indent-region.") + +(defun matlab--indent-line (lvl2) + "Indent the current line according to MATLAB mode. +Input LVL2 is a pre-scanned context from `matlab-compute-line-context' lvl2. +Used internally by `matlab-indent-line', and `matlab-indent-region'." + (let* ((i (matlab--calc-indent lvl2))) + (funcall matlab--change-indentation-override i))) + +(defun matlab--change-indentation (new-indentation) + "Change the indentation on line to NEW-INDENTATION. +This function exists so the test harness can override it." + (let* ((i (max new-indentation 0)) + (ci (current-indentation)) + (cc (current-column)) + (diff (- ci i))) (save-excursion (back-to-indentation) - (if (= i (current-column)) - nil - (beginning-of-line) - (delete-horizontal-space) - (indent-to i))) - (if (<= cc ci) (move-to-column i)) - )) + (cond ((= diff 0) ;; Already a match - do nothing. + nil) + ((< diff 0) ;; Too short - Add stuff + (indent-to i)) + ((<= diff ci) ;; Too much, delete some. + (delete-region (save-excursion (move-to-column i t) (point)) (point)) + ) + (t ;; some sort of bug that wants to delete too much. Ignore. + nil) + )) + (if (<= cc ci) (move-to-column (max 0 i))) + (/= 0 diff))) -(defun matlab-calc-indent () +(defun matlab--calc-indent (&optional lvl2 debug-sym) "Return the appropriate indentation for this line as an integer." - (interactive) + ;; In case it wasn't provided. + (unless lvl2 (setq lvl2 (matlab-compute-line-context 2))) ;; The first step is to find the current indentation. ;; This is defined to be zero if all previous lines are empty. - (let* ((ci (save-excursion (if (not (matlab-prev-line)) - 0 - (matlab-next-line-indentation)))) - (sem (matlab-calculate-indentation ci))) + (let* ((sem (matlab-calculate-indentation lvl2))) + (when debug-sym + (set debug-sym sem)) ;; simplistic (nth 1 sem))) +(defun matlab--previous-line-indent-recommendation (lvl2) + "Return the indentation recommendation from the previous line of CODE. +Uses the lvl2 context of the current line of code it scans backward from. +This function scans backward over blank lines and comments to find a +line of code. It then scans that line and recommends either: + - same indentation - if just boring old code. + - indent more - if has control block openings on it." + (cond + ((save-excursion (beginning-of-line) (bobp)) + ;; Beginning of buffer - do no work, just return 0. + 0) + ((matlab-line-close-paren-outer-point (matlab-get-lvl1-from-lvl2 lvl2)) + ;; If we are inside an array continuation, then we shouldn't + ;; need to do anything complicated here b/c we'll just ignore + ;; the returned value in the next step. Return current indentation + ;; of the previous non-empty line. + (matlab-line-indentation (matlab-previous-nonempty-line lvl2))) + (t + ;; Else, the previous line might recommend an indentation based + ;; on it's own context, like being a block open or continuation. + (let ((prevcmd (matlab-previous-code-line lvl2))) + (matlab-with-context-line prevcmd + (cond + ((and (matlab-line-empty-p prevcmd) + (save-excursion (beginning-of-line) (bobp))) + ;; Beginning of buffer - do no work, just return 0. + 0) + (t + (matlab-next-line-indentation prevcmd)))))))) + + (defconst matlab-functions-have-end-should-be-true "This end closes a function definition.\nDo you want functions to have ends? " "Prompt the user about whether to change `matlab-functions-have-end'.") -(defun matlab-calculate-indentation (current-indentation) + +(defun matlab--maybe-yes-or-no-p (prompt noninteractive-default) + "When in non-interactive mode run (yes-or-no-p prompt), +otherwise return NONINTERACTIVE-DEFAULT" + (if noninteractive + noninteractive-default + (yes-or-no-p prompt))) + + +(defun matlab-calculate-indentation (&optional lvl2) "Calculate out the indentation of the current line. Return a list of descriptions for this line. Return format is: '(TYPE DEPTHNUMBER) where TYPE is one of (comment, code, function, blockstart, blockmid, blockendless, blockend) DEPTHNUMBER is how many characters to indent -this line. - Argument CURRENT-INDENTATION is what the previous line thinks -this line's indentation should be. See `matlab-next-line-indentation'." - (matlab-navigation-syntax - (matlab-calculate-indentation-1 current-indentation))) - -(defun matlab-calculate-indentation-1 (current-indentation) - "Do the indentation work of `matlab-calculate-indentation'. -Argument CURRENT-INDENTATION is what the previous line recommends for indentation." - (let ((ci current-indentation) +this line." + (unless lvl2 (setq lvl2 (matlab-compute-line-context 2))) + + (let ((lvl1 (matlab-get-lvl1-from-lvl2 lvl2)) (tmp nil)) (cond ;; COMMENTS - ((matlab-ltype-comm) - (cond - ;; HELP COMMENT and COMMENT REGION - ((or (setq tmp (matlab-ltype-help-comm)) - (matlab-ltype-comm-ignore)) - (list 'comment-help - (if tmp - (save-excursion - (goto-char tmp) - (current-indentation)) - (save-excursion - (matlab-beginning-of-defun) - (current-indentation))))) - ;; BLOCK COMMENT END _or_ body prefixed with % - ((and matlab-ltype-block-comm-bounds - (or (matlab-ltype-block-comm-at-end) - (matlab-ltype-comm-noblock))) - (list 'comment (save-excursion - (goto-char (car matlab-ltype-block-comm-bounds)) - (current-indentation))) - ) - ;; BLOCK COMMENT START - ((and matlab-ltype-block-comm-bounds - (matlab-ltype-block-comm-at-start)) - (list 'comment (+ ci matlab-comment-anti-indent)) - ) - ;; BLOCK COMMENT BODY. - (matlab-ltype-block-comm-bounds - (list 'comment - (+ (save-excursion - (goto-char (car matlab-ltype-block-comm-bounds)) - (current-indentation)) - 2)) - ) - ;; COMMENT Continued From Previous Line - ((setq tmp (matlab-ltype-continued-comm)) - (list 'comment tmp)) - (t - (list 'comment (+ ci matlab-comment-anti-indent))))) + ((matlab-line-comment-p lvl1) + (let ((comment-style (matlab-line-comment-style lvl1))) + (cond + ;; BLOCK END undoes body indent + ((or (eq comment-style 'block-end) + (eq comment-style 'block-body-prefix)) ; body prefix has same lineup rule + (list 'comment (matlab-line-end-comment-column lvl1))) + ;; BLOCK BODY is indented slightly from the start. + ((eq comment-style 'block-body) + (list 'comment (+ 2 (matlab-line-end-comment-column lvl1)))) + ;; HELP COMMENT and COMMENT REGION + ((and (matlab-last-guess-decl-p) + (setq tmp (matlab-scan-comment-help-p lvl2))) + (list 'comment-help tmp)) + ;; BLOCK START is like regular comment + ((eq comment-style 'block-start) + ;; indent like code, but some users like anti-indent + (list 'comment (+ (matlab--previous-line-indent-recommendation lvl2) matlab-comment-anti-indent)) + ) + ;; COMMENT REGION comments + ((matlab-line-comment-ignore-p lvl1) + (list 'comment-ignore 0)) + ;; COMMENT Continued From Previous Line + ((setq tmp (matlab-line-continued-comment)) ;;; TODO : REPLACE + (list 'comment tmp)) + (t + (list 'comment (+ (matlab--previous-line-indent-recommendation lvl2) matlab-comment-anti-indent)))))) + ;; FUNCTION DEFINITION - ((matlab-ltype-function-definition) - (if matlab-functions-have-end - ;; A function line has intrinsic indentation iff function bodies are - ;; not indented and the function line is nested within another function. - (if (and (not (matlab-indent-function-body-p)) - (save-excursion - (beginning-of-line) - (matlab-beginning-of-enclosing-defun))) - (setq ci (+ ci matlab-indent-level)) - ;; If no intrinsic indentation, do not change from ci. + ((matlab-line-declaration-p lvl1) + (cond ((not matlab-functions-have-end) + (setq tmp 0)) + ;; If we do have ends, check if we are nexted + ;; A function line has intrinsic indentation iff function bodies are + ;; not indented and the function line is nested within another function. + ((and (not (matlab-indent-function-body-p)) + (save-excursion + (beginning-of-line) + ;; TODO - maybe replace this? Not usually used. + (matlab--scan-block-backward-up-until 'decl))) + (setq tmp (+ (matlab--previous-line-indent-recommendation lvl2) matlab-indent-level))) + (t + ;; If no intrinsic indentation, do not change from current-indentation. + (setq tmp (matlab--previous-line-indent-recommendation lvl2))) ) - ;; If functions are not nested, functions go to left margin. - (setq ci 0)) - (list 'function ci)) + (list 'function tmp)) + ;; END keyword - ((matlab-lattr-local-end) - (let ((end-of-function - (let ((matlab-functions-have-end t)) - (save-excursion - (beginning-of-line) - (matlab-backward-sexp t) ;; may throw "unstarted block" error - (matlab-ltype-function-definition))))) - (if end-of-function + ((matlab-line-end-p lvl1) + (let* ((CTXT (matlab-with-context-line lvl1 + (matlab-scan-block-start-context)))) + + (if (eq (car CTXT) 'decl) ;; declarations (ie - function) is treated special. (if (or matlab-functions-have-end - (if (yes-or-no-p matlab-functions-have-end-should-be-true) - ;; TODO - ask user to reindent the fcn now? - (setq matlab-functions-have-end t) + (if (matlab--maybe-yes-or-no-p matlab-functions-have-end-should-be-true t) + (matlab-functions-have-end-minor-mode 1) (error "Unmatched end"))) + (if (matlab-indent-function-body-p) - (setq ci (- ci matlab-indent-level)))) - ;; Next, see if this line starts with an end, and whether the - ;; end is matched, and whether the line is blank up to the match. - ;; If so, return the indentation of the match. - (catch 'indent - (save-excursion - (when (progn (beginning-of-line) - (and (looking-at "[ \t]*end\\b") - (matlab-backward-sexp t t))) - (let ((match (point))) - (beginning-of-line) - (looking-at "[ \t]*") - (when (= match (match-end 0)) - (let ((match-col-end - (save-excursion - (goto-char match) - (current-column))) - (match-col-beginning - (save-excursion - (goto-char (match-beginning 0)) - (current-column))) - ) - (setq ci (- match-col-end match-col-beginning))) - (throw 'indent nil))))) - ;; End of special case for end and match after "^[ \t]*". - (setq ci (+ ci - (* (1- (matlab-lattr-block-cont (point))) - matlab-indent-level)))))) - (list 'blockend ci)) + ;; Match indentation of the function regardless of any other + ;; state that might have gotten messed up. + (setq tmp (matlab-line-indentation (nth 3 CTXT))) + + ;; Else, no change + (setq tmp (matlab--previous-line-indent-recommendation lvl2)) + )) + + ;; Not a declaration. In that case, just match up with the + ;; line that the block stat is on. + (setq tmp (matlab-line-indentation (nth 3 CTXT))))) + (list 'blockend tmp)) + ;; ELSE/CATCH keywords - ((matlab-lattr-middle-block-cont) - (let ((m (match-string 1))) - (list 'blockmid - (condition-case nil - (save-excursion - (beginning-of-line) - (matlab-backward-sexp t) - (if (matlab-ltype-function-definition) (error "")) - (current-column)) - (error (error "Unmatched %s" m)))))) + ((matlab-line-block-middle-p lvl1) + (list 'blockmid + (save-excursion + (back-to-indentation) + (if (matlab--scan-block-backward-up) + (error "Missing start block") + (if (not (eq (matlab-on-keyword-p) 'ctrl)) + (error "Does not match opening block type")) + (current-column))))) + ;; CASE/OTHERWISE keywords - ((matlab-lattr-endless-block-cont) + ((matlab-line-block-case-p lvl1) (list 'blockendless - (condition-case nil - (save-excursion - (beginning-of-line) - (matlab-backward-sexp t) - (if (not (looking-at "switch\\>")) (error "")) + (save-excursion + (back-to-indentation) + (if (matlab--scan-block-backward-up) + (error "Missing switch") + (if (not (eq (matlab-on-keyword-p) 'ctrl)) + (error "Wrong type of start block") (+ (current-column) - (if (listp matlab-case-level) - (car matlab-case-level) - matlab-case-level))) - (error (error "Unmatched case/otherwise part"))))) - ;; End of a MATRIX - ((matlab-lattr-array-end) - (list 'array-end (save-excursion - (back-to-indentation) - (matlab-up-list -1) - (let* ((fc (following-char)) - (mi (assoc fc matlab-maximum-indents)) - (max (if mi (if (listp (cdr mi)) - (car (cdr mi)) (cdr mi)) - nil)) - (ind (if mi (if (listp (cdr mi)) - (cdr (cdr mi)) (cdr mi)) - nil))) - ;; apply the maximum limits. - (if (and ind (> (- (current-column) ci) max)) - (1- ind) ; decor - (current-column)))))) - ;; Code lines - ((and (not (matlab-lattr-array-cont)) - (not (matlab-prev-line-cont))) + (if (listp matlab-case-indent-level) + (car matlab-case-indent-level) + matlab-case-indent-level))))))) + + ;; END of a MATRIX + ((matlab-line-close-paren-p lvl1) + (list 'array-end (let* ((fc (matlab-line-close-paren-inner-char lvl1)) + (pc (matlab-line-close-paren-inner-col lvl1)) + (mi (assoc fc matlab-maximum-indents)) + (max (if mi (if (listp (cdr mi)) + (car (cdr mi)) (cdr mi)) + nil)) + (ind (if mi (if (listp (cdr mi)) + (cdr (cdr mi)) (cdr mi)) + nil))) + ;; apply the maximum limits. + (if (and ind (> (- pc (matlab--previous-line-indent-recommendation lvl2)) max)) + (1- ind) ; decor + pc)))) + + ;; CODE LINES + ((and (not (matlab-line-close-paren-outer-point lvl1)) + (not (matlab-scan-previous-line-ellipsis-p))) + ;; Code always matches up against the previous line. + (list 'code (matlab--previous-line-indent-recommendation lvl2))) + + ;; CONTINUATION but from within a parenthetical: A group of cases for continuation + ((matlab-line-close-paren-inner-col lvl1) + (let* ((boc-lvl1 (save-excursion + (matlab-scan-beginning-of-command) + (matlab-compute-line-context 1))) + (ci-boc (matlab-line-indentation boc-lvl1)) + (boc (matlab-line-point boc-lvl1)) - ;; Old check for base code line was using - ;; 'matlab-beginning-of-command' which does a lot of work, like - ;; checking for block comments and such. We already stripped - ;; out most options in that cmd and know we are on a code line, - ;; so only check if the previous line is a continuation line. - ;; Old code: - ;;(save-excursion - ;; (beginning-of-line) - ;; (back-to-indentation) - ;; (= (point) (progn (matlab-beginning-of-command) (point)))) + ;; Scratch vars for paren stuff + (parencol (matlab-line-close-paren-inner-col lvl1)) + (parenchar (matlab-line-close-paren-inner-char lvl1)) + (parenpt (matlab-line-close-paren-inner-point lvl1)) + (parenindent (when parenpt + (save-excursion (goto-char parenpt) + (current-indentation)))) + (parenopt (matlab-line-close-paren-outer-point lvl1)) + + + ;; What shall we use to describe this for debugging? + (indent-type (cond ((and parenchar (= parenchar ?\()) 'function-call-cont) + ((and parencol (= parenindent parencol)) 'array-solo-cont) + ((and parenpt (/= parenpt parenopt)) 'nested-array-cont) + (t 'code-cont))) ;; last not likely. + + (found-column nil) + ) + + (save-excursion + (cond + ((and + ;; CONTINUATION with FUNCTIONs that indent past arg1 + (eq indent-type 'function-call-cont) + ;; This checks for our special set of functions. + (save-excursion + (goto-char parenpt) + (forward-symbol -1) + (looking-at + matlab-indent-past-arg1-functions)) + ;; We are in a fcn call, AND the fcn wants to + ;; indent past the first argument. Only do so + ;; if first arg is a SIMPLE EXPR. + (matlab-navigation-syntax + (goto-char parenpt) + (looking-at "(\\s-*\\(?:\\w\\|\\.\\)+\\s-*,") + (setq found-column (match-end 0))) + (save-excursion + (goto-char found-column) ; move to comma + ;; Don't bother if we hit the EOL. + (not (looking-at "\\s-*\\(\\.\\.\\.\\|$\\|)\\)")))) + ;; We are in the right kind of place. Lets + ;; start indenting + (goto-char found-column) + (skip-chars-forward " \t") + (if (> (- (current-column) parencol) + matlab-arg1-max-indent-length) + (setq tmp (+ parencol matlab-arg1-max-indent-length)) + (setq tmp (current-column)))) + + ;; CONTINUATION with PARENS, BRACKETS, etc + (t + (let* ((mi (assoc parenchar matlab-maximum-indents)) + (max (if mi (if (listp (cdr mi)) (car (cdr mi)) (cdr mi)) nil)) + (ind (if mi (if (listp (cdr mi)) (cdr (cdr mi)) (cdr mi)) nil))) + (goto-char parenpt) + (forward-char 1) + (skip-chars-forward " \t") + ;; If we are at the end of a line and this + ;; open paren is there, then we DON'T want + ;; to indent to it. Use the standard + ;; indent. + (if (or (not matlab-align-to-paren) + (looking-at "\\.\\.\\.\\|$")) + (if (or (eq indent-type 'function-call-cont) + (and (not (eq indent-type 'array-solo-cont)) + (not (eq indent-type 'nested-array-cont)))) + ;; functions or an array ending on a EOL should + ;; do normal code indentation from beginning of cmd + (setq tmp (+ ci-boc matlab-continuation-indent-level)) + ;; If in an array in an array ending on EOL should + ;; indent a wee bit + (setq tmp (+ parencol matlab-array-continuation-indent-level))) + ;; current column is location on original line where + ;; first bit of text is, so line up with that. + (setq tmp (current-column)) + ;; TODO - this disables indentation MAXs + ;; if we really want to be rid of this + ;; we can dump a bunch of logic above too. + ;; apply the maximum limits. + ;;(if (and ind (> (- (current-column) ci-boc) max)) + ;; (+ ci-boc ind) + ;; (current-column)) + ))) + )) + (list indent-type tmp))) - ;; This means we are at the beginning of a command structure. - ;; Always match up against the previous line. - (list 'code ci)) - ;; Lines continued from previous statements. (t - (list (if (matlab-ltype-empty) 'empty - (if (matlab-lattr-array-cont) 'array-cont 'code)) - ;; Record beginning of the command - (let ((boc (save-excursion - (matlab-beginning-of-command) - (point)))) - (condition-case nil - (save-excursion - (beginning-of-line) - (matlab-up-list -1) - (if (> boc (point)) (error nil)) - ;; Ok, it MIGHT be that we are in a program - ;; statement, and this particular command is an HG - ;; statement that would look better if the - ;; following lines lined up AFTER the first - ;; argument. Lets look. - (let ((parendepth (current-column))) - (cond ((and (= (following-char) ?\( ) - (save-excursion - (matlab-navigation-syntax - (forward-word -1) - (looking-at - matlab-indent-past-arg1-functions))) - (let ((start-paren (point))) - (while - (and - (re-search-forward - "," (matlab-point-at-eol) t) - (save-excursion - (matlab-up-list -1) - (> (point) start-paren)))) - (if (and - (= (preceding-char) ?,) - ;; Don't bother if we hit the EOL. - (not (looking-at + ;; Other kinds of continuations + (let* ((prev-lvl1 (save-excursion + (forward-line -1) + (matlab-compute-line-context 1))) + (prev2-lvl1 (save-excursion + (forward-line -2) + (matlab-compute-line-context 1))) + (ci-prev (matlab-line-indentation prev-lvl1)) + + (boc (matlab-line-point prev-lvl1)) + (boc2 (matlab-line-point prev2-lvl1)) - "\\s-*\\(\\.\\.\\.\\|$\\|)\\)"))) - t - (move-to-column parendepth) - nil))) - (skip-chars-forward " \t") - (if (> (- (current-column) parendepth) - matlab-arg1-max-indent-length) - (+ parendepth matlab-arg1-max-indent-length) - (current-column))) - (t - (let* ((fc (following-char)) - (mi (assoc fc matlab-maximum-indents)) - (max (if mi - (if (listp (cdr mi)) - (car (cdr mi)) (cdr mi)) - nil)) - (ind (if mi - (if (listp (cdr mi)) - (cdr (cdr mi)) (cdr mi)) - nil))) - (forward-char 1) - (skip-chars-forward " \t") - ;; If we are at the end of a line and - ;; this open paren is there, then we - ;; DON'T want to indent to it. Use the - ;; standard indent. - (if (or (not matlab-align-to-paren) - (looking-at "\\.\\.\\.\\|$")) - ;; This could happen in another set - ;; of matrices. Find a current - ;; indentation based on the - ;; previous line. - (let ((cci (current-indentation))) - (+ cci matlab-cont-level)) - ;; apply the maximum limits. - (if (and ind (> (- (current-column) ci) max)) - (+ ci ind) - (current-column)))))))) - (error - ;; Line up to an equals sign. - (save-excursion - (goto-char boc) - (while (and (re-search-forward "=" (matlab-point-at-eol) t) - (matlab-cursor-in-string-or-comment))) - (if (/= (preceding-char) ?=) - (+ ci matlab-cont-level) - (skip-chars-forward " \t") - (let ((cc (current-column)) - (mi (assoc ?= matlab-maximum-indents))) - (if (looking-at "\\.\\.\\.\\|$") - ;; In this case, the user obviously wants the - ;; indentation to be somewhere else. - (+ ci (cdr (cdr mi))) - ;; If the indent delta is greater than the max, - ;; use the max + current - (if (and mi (> (- cc ci) (if (listp (cdr mi)) - (car (cdr mi)) - (cdr mi)))) - (setq cc (+ ci (if (listp (cdr mi)) - (cdr (cdr mi)) - (cdr mi))))) - cc))))))))) + (indent-type (cond ((matlab-line-empty-p lvl1) 'empty) + (t 'code-cont))) + (found-column nil) + ) + + (save-excursion + (cond + ;; Beginning of CONTINUATION has EQUALS + ((and (or (not (matlab-line-ellipsis-p prev2-lvl1)) + (= boc boc2)) + (save-excursion + (goto-char boc) + (while (and (re-search-forward "=" (matlab-point-at-eol) t) + (matlab-cursor-in-string-or-comment))) + (when (= (preceding-char) ?=) + (skip-chars-forward " \t") + (setq found-column (point))) + )) + (save-excursion + (goto-char found-column) + (let ((cc (current-column)) + (mi (assoc ?= matlab-maximum-indents)) + (prev-indent (matlab--previous-line-indent-recommendation lvl2))) + + (if (looking-at "\\.\\.\\.\\|$") + ;; In this case, the user obviously wants the + ;; indentation to be somewhere else. + (setq tmp (+ prev-indent (cdr (cdr mi)))) + ;; If the indent delta is greater than the max, + ;; use the max + current + (if (and mi (> (- cc prev-indent) (if (listp (cdr mi)) (car (cdr mi)) (cdr mi)))) + (setq tmp (+ prev-indent (if (listp (cdr mi)) (cdr (cdr mi)) (cdr mi)))) + (setq tmp cc)))))) + + ;; CONTINUATION with nothing special about it. + ((or (not (matlab-line-ellipsis-p prev2-lvl1)) + (= boc boc2)) + + ;; Continued from non-continued line, push in just a little + ;; Do explicit call to next-line-indentation b/c we've already computed the lvl1 context + ;; on the beginning of command. + (setq tmp (+ (matlab-next-line-indentation prev-lvl1) matlab-continuation-indent-level))) + + ;; CONTINUATION from a continued line, nothing special + (t + ;; Just match the same + (setq tmp ci-prev)) + + )) + + (list indent-type tmp) + )) ))) -(defun matlab-next-line-indentation () +(defun matlab-next-line-indentation (lvl1) "Calculate the indentation for lines following this command line. -Assume that the following line does not contribute its own indentation -\(as it does in the case of nested functions in the following situations): - o function---positive indentation when not indenting function bodies. - o end---negative indentation except when the 'end' matches a function and - not indenting function bodies. -See `matlab-calculate-indentation'." - (matlab-navigation-syntax - (let ((startpnt (point-at-eol))) - (save-excursion - (matlab-with-current-command - ;;(matlab-beginning-of-command) - (goto-char (point-min)) - (back-to-indentation) - (let ((cc (or (matlab-lattr-block-close startpnt) 0)) - (end (matlab-lattr-local-end)) - (bc (matlab-lattr-block-cont startpnt)) - (mc (matlab-lattr-middle-block-cont)) - (ec (matlab-lattr-endless-block-cont)) - (hc (and (matlab-indent-function-body-p) (matlab-ltype-help-comm))) - (rc (and (/= 0 matlab-comment-anti-indent) - (matlab-ltype-comm-noblock) - (not (matlab-ltype-help-comm)) - (not (matlab-ltype-continued-comm)))) - (ci (current-indentation))) - ;; When the current point is on a line with a function, the value of bc will - ;; reflect the function in a block count iff if matlab-functions-have-end is - ;; true. However, if matlab-indent-function-body-p is false, there should be - ;; no actual indentation, so bc needs to be decremented by 1. Similarly, if - ;; on a line with an end that closes a function, bc needs to be decremented - ;; by 1 if matlab-functions-have-end is true and matlab-indent-function-body-p - ;; is false. However, just to be safe, indentation is not allowed to go - ;; negative. Thus: - (if matlab-functions-have-end - (if (and - (not (matlab-indent-function-body-p)) - (or (matlab-ltype-function-definition) - (and (matlab-lattr-local-end) - (save-excursion - (matlab-backward-sexp t) - (looking-at "function\\b"))))) - (if (> bc 0) - (setq bc (1- bc)) - (if (>= ci matlab-indent-level) - (setq bc -1)))) - (if (and (matlab-indent-function-body-p) (matlab-ltype-function-definition)) - (setq bc (1+ bc)))) - ;; Remove 1 from the close count if there is an END on the beginning - ;; of this line, since in that case, the unindent has already happened. - (when end (setq cc (1- cc))) - ;; Calculate the suggested indentation. - (+ ci - (* matlab-indent-level bc) - (* matlab-indent-level (or mc 0)) - (* matlab-indent-level (- cc)) - (* (if (listp matlab-case-level) - (cdr matlab-case-level) matlab-case-level) - (or ec 0)) - (if hc matlab-indent-level 0) - (if rc (- 0 matlab-comment-anti-indent) 0) - ))))))) - -;;; The return key ============================================================ +See `matlab-calculate-indentation' for how the output of this fcn is used." + (let ((startpnt (matlab-with-context-line lvl1 + (point-at-eol))) + ) + (save-excursion + (matlab-scan-beginning-of-command lvl1) + + (let* ((boc-lvl1 (matlab-compute-line-context 1)) + (depthchange (matlab-line-count-block-change boc-lvl1 lvl1)) + (end (matlab-line-end-p boc-lvl1)) + ) + + ;; When DEPTHCHANGE is negative, and END is false, or + ;; DEPTHCHANGE < -1, then the NEXT line should have an indent + ;; that matches the context at the beginning of the block of + ;; the last end. + ;; + ;; If we don't do this, and a block-start is not the FIRST item + ;; on a line, then there is no way for the following line to figure + ;; out where it should be. + (if (or (< depthchange -1) (and (= depthchange -1) end)) + (let* ((CTXT (matlab-with-context-line boc-lvl1 + (matlab-line-end-of-code boc-lvl1) + (matlab-re-search-keyword-backward + (matlab-keyword-regex 'end) (point-at-bol) t) + (matlab-scan-block-start-context)))) + (matlab-line-indentation (nth 3 CTXT))) -(defcustom matlab-return-function 'matlab-indent-end-before-ret - "Function to handle return key. -Must be one of: - 'matlab-plain-ret - 'matlab-indent-after-ret - 'matlab-indent-end-before-ret - 'matlab-indent-before-ret" - :group 'matlab - :type '(choice (function-item matlab-plain-ret) - (function-item matlab-indent-after-ret) - (function-item matlab-indent-end-before-ret) - (function-item matlab-indent-before-ret))) + ;; Indent recommendations not related to ENDS + + ;; If we are NOT indenting our functions and we are on + ;; a declaration, then we should subtract 1 from the beginning count. + ;; This fairly simple change removes a big chunk of the old code. + (when (and (not (matlab-indent-function-body-p)) + (matlab-line-declaration-p boc-lvl1)) + (setq depthchange (1- depthchange))) + + ;; Remove 1 from the close count if there is an END on the beginning + ;; of this line, since in that case, the unindent has already happened. + (when end (setq depthchange (1+ depthchange))) + + ;; Calculate the suggested indentation. + (+ (current-indentation) + (* matlab-indent-level depthchange) + (* matlab-indent-level (if (matlab-line-block-middle-p boc-lvl1) 1 0)) + (* (cdr matlab-case-indent-level) (if (matlab-line-block-case-p boc-lvl1) 1 0)) + )))))) -(defun matlab-return () - "Handle carriage return in `matlab-mode'." - (interactive) - (matlab-semicolon-on-return) - (funcall matlab-return-function)) - -(defun matlab-plain-ret () - "Vanilla new line." - (interactive) - (newline)) - -(defun matlab-indent-after-ret () - "Indent after new line." - (interactive) - (newline) - (matlab-indent-line)) - -(defun matlab-indent-end-before-ret () - "Indent line if block end, start new line, and indent again." - (interactive) - (if (save-excursion - (beginning-of-line) - (looking-at (concat "^\\s-*\\(" (matlab-block-end-re) - "\\|" (matlab-block-mid-re) - "\\|" (matlab-endless-blocks-re) - "\\|function\\)"))) - (matlab-indent-line)) - (newline) - (matlab-indent-line)) - -(defvar matlab-quiesce-nosemi-regexp) ;; quiet compiler warning (var is defined below) -(defun matlab-semicolon-on-return () - "If needed, add a semicolon at point automatically." - (if matlab-return-add-semicolon - (if (and (not (matlab-ltype-empty)) - (not (save-excursion - (skip-chars-backward " \t;" (matlab-point-at-bol)) - (looking-at "\\s-*;"))) - (save-excursion - (let ((p (point))) - (matlab-end-of-command (point)) - (eq p (point)))) - (save-excursion - (matlab-beginning-of-command) - ;; Note: Compile warning below, but defined later. - (not (looking-at matlab-quiesce-nosemi-regexp)))) - (insert ";")) - )) - -(defun matlab-indent-before-ret () - "Indent line, start new line, and indent again." - (interactive) - (matlab-indent-line) - (newline) - (matlab-indent-line)) - -(defun matlab-linefeed () - "Handle line feed in `matlab-mode'. -Has effect of `matlab-return' with (not matlab-indent-before-return)." - (interactive) - (matlab-indent-line) - (newline) - (matlab-indent-line)) +(defun matlab-electric-indent-function (char) + "Return t if `electric-indent-mode' should indent after CHAR is inserted. +Return nil otherwise. +This function recommends indenting after special keywords that typically cause indentation +changes so the code fixes itself up." + (cond ((eq char ?e) + (let ((lvl1 (matlab-compute-line-context 1))) + (or (matlab-line-block-middle-p lvl1) + (matlab-line-block-case-p lvl1)))) + ((eq char ?d) + (let ((lvl1 (matlab-compute-line-context 1))) + (matlab-line-end-p lvl1))) + (t + nil))) ;;; Comment management======================================================== @@ -3235,139 +2257,92 @@ Has effect of `matlab-return' with (not matlab-indent-before-return)." (defun matlab-comment-return () "Handle carriage return for MATLAB comment line." (interactive) - (cond - ((matlab-ltype-comm) - (matlab-set-comm-fill-prefix) (newline) (insert fill-prefix) - (matlab-reset-fill-prefix) (matlab-indent-line)) - ((matlab-lattr-comm) - (newline) (indent-to comment-column) - (insert matlab-comment-on-line-s)) - (t - (newline) (matlab-comment) (matlab-indent-line)))) - -(defun matlab-comm-from-prev () - "If the previous line is a `comment-line' then set up a comment on this line." - (save-excursion - ;; If the previous line is a comment-line then set the fill prefix from - ;; the previous line and fill this line. - (if (and (= 0 (forward-line -1)) (matlab-ltype-comm)) - (progn - (matlab-set-comm-fill-prefix) - (forward-line 1) (beginning-of-line) - (delete-horizontal-space) - (if (looking-at "%") (delete-char 1)) - (delete-horizontal-space) - (insert fill-prefix) - (matlab-reset-fill-prefix))))) + (newline) + (matlab-comment)) (defun matlab-electric-comment (arg) "Indent line and insert comment character. Argument ARG specifies how many %s to insert." (interactive "P") (self-insert-command (or arg 1)) - (when (matlab-ltype-comm) + (when (or (eq last-command-event ?%) + (and (eq last-command-event ?^) ;; ignore comments move quick back to col0 + (matlab-line-comment-p (matlab-compute-line-context 1)) + (eq (char-before (1- (point))) ?%) + )) (matlab-indent-line) - ;; The above seems to put the cursor on the %, not after it. - (skip-chars-forward "%"))) + (skip-chars-forward "%^"))) (defun matlab-electric-block-comment (arg) "Indent line and insert block comment end character. Argument ARG specifies how many %s to insert." (interactive "P") (self-insert-command (or arg 1)) - (let ((bc (save-excursion (beginning-of-line) (matlab-ltype-block-comm)))) + (let* ((lvl1 (matlab-compute-line-context 1)) + (bc (matlab-line-block-comment-start lvl1))) - (cond ((matlab-ltype-block-comment-start) - - ;; Starting block comment. Check if we are already in a block - ;; comment, and blink it if a problem. - (let ((bcwrapped (save-excursion - (beginning-of-line) - (matlab-ltype-block-comm)))) - - ;; Regardless, indent our line + (cond ((eq (matlab-line-comment-style lvl1) 'block-start) (matlab-indent-line) - - (when bcwrapped - (save-excursion - (goto-char (car bcwrapped)) - (skip-chars-forward "%{") - (message "Nested block comment start %%{") - (pulse-momentary-highlight-region (car bcwrapped) (point)))) - )) - - ;;ELSE, maybe end of block comment - ((and bc (matlab-ltype-block-comm-at-end)) - (progn - (matlab-indent-line) - ;; The above sometimes puts the cursor on the %, not after it. - (skip-chars-forward "%}") - (pulse-momentary-highlight-region (car bc) (cdr bc))) ) - - ;; Else, not in a comment - which means we don't have - ((and (not bc) (save-excursion - (skip-chars-backward "%{") - (looking-at "%{"))) - (message "Block comment end has no matching %%{") - (save-excursion - (beginning-of-line) - (when (re-search-backward matlab-block-comment-end-re nil t) - (pulse-momentary-highlight-region (match-beginning 0) (match-end 0)))) + ;;ELSE, maybe end of block comment + ((eq (matlab-line-comment-style lvl1) 'block-end) + (matlab-indent-line) + (when bc (pulse-momentary-highlight-region bc (point))) ) ))) (defun matlab-comment () "Add a comment to the current line." (interactive) - (cond ((region-active-p) - (call-interactively #'comment-or-uncomment-region)) - ((matlab-ltype-empty) ; empty line - (matlab-comm-from-prev) - (if (matlab-lattr-comm) - (skip-chars-forward " \t%") + (let ((lvl1 (matlab-compute-line-context 1))) + (cond ((region-active-p) + (call-interactively #'comment-or-uncomment-region)) + + ((matlab-line-empty-p lvl1) ; empty line (insert matlab-comment-line-s) - (matlab-indent-line))) - ((matlab-ltype-comm) ; comment line - (matlab-comm-from-prev) - (skip-chars-forward " \t%")) - ((matlab-lattr-comm) ; code line w/ comment - (beginning-of-line) - (re-search-forward "[^%]\\(%\\)[ \t]") - (goto-char (match-beginning 1)) - (if (> (current-column) comment-column) (delete-horizontal-space)) - (if (< (current-column) comment-column) (indent-to comment-column)) - ;; Now see if the current line is too long to fit. Can we back indent? - (let ((eol-col (- (point-at-eol) (point-at-bol)))) - (when (> eol-col fill-column) - (delete-horizontal-space) - (indent-to (- comment-column (- eol-col fill-column))))) - (skip-chars-forward "% \t")) - (t ; code line w/o comment - (end-of-line) - (re-search-backward "[^ \t\n^]" 0 t) - (forward-char) - (delete-horizontal-space) - (if (< (current-column) comment-column) - (indent-to comment-column) - (insert " ")) - (insert matlab-comment-on-line-s)))) + (back-to-indentation) + (matlab-indent-line) + (skip-chars-forward " \t%")) + + ((matlab-line-comment-p lvl1) ; comment line + (back-to-indentation) + (matlab-indent-line) + (skip-chars-forward " \t%")) + + ((matlab-line-end-comment-column lvl1) ; code line w/ comment + (goto-char (matlab-line-end-comment-point lvl1)) + (if (> (current-column) comment-column) (delete-horizontal-space)) + (if (< (current-column) comment-column) (indent-to comment-column)) + ;; Now see if the current line is too long to fit. Can we back indent? + (let ((eol-col (- (point-at-eol) (point-at-bol)))) + (when (> eol-col fill-column) + (delete-horizontal-space) + (indent-to (- comment-column (- eol-col fill-column))))) + (skip-chars-forward "% \t")) + + (t ; code line w/o comment + (end-of-line) + (re-search-backward "[^ \t\n^]" 0 t) + (forward-char) + (delete-horizontal-space) + (if (< (current-column) comment-column) + (indent-to comment-column) + (insert " ")) + (insert matlab-comment-on-line-s))))) (defun matlab-comment-line-break-function (&optional soft) "Break the current line, and if in a comment, continue it. Optional argument SOFT indicates that the newline is soft, and not hard." (interactive) (if (not (matlab-cursor-in-comment)) - (matlab-return) + (progn (newline);;(matlab-return) + (matlab-indent-line)) ;; Will the below fn work in old emacsen? (if soft (insert-and-inherit ?\n) (newline 1)) (insert "% ") (matlab-indent-line) - (end-of-line))) - -(defun matlab-comment-indent () - "Indent a comment line in `matlab-mode'." - (matlab-calc-indent)) + (back-to-indentation) + (skip-chars-forward "% "))) (defun matlab-comment-region (beg-region end-region arg) "Comments every line in the region. @@ -3406,7 +2381,7 @@ Argument BEG and END indicate the region to uncomment." (defun matlab-set-comm-fill-prefix () "Set the `fill-prefix' for the current (comment) line." (interactive) - (if (matlab-lattr-comm) + (if (matlab-line-comment-p (matlab-compute-line-context 1)) (setq fill-prefix (save-excursion (beginning-of-line) @@ -3436,114 +2411,116 @@ unless we decide we can fudge the numbers. Return nil if this line should not be broken. This function will ONLY work on code." ;; First of all, if this is a continuation, then the user is ;; requesting that we don't mess with his stuff. - (if (matlab-lattr-cont) - nil - (save-restriction - (narrow-to-region (matlab-point-at-bol) (matlab-point-at-eol)) - ;; get ourselves onto the fill-column. - (move-to-column fill-column) - (let ((pos nil) - (orig (point))) - (or - ;; Next, if we have a trailing comment, use that. - (progn (setq pos (or (matlab-lattr-comm) (matlab-point-at-bol))) - (goto-char pos) - (if (and (> (current-column) (- fill-column matlab-fill-fudge)) - (< (current-column) (+ fill-column matlab-fill-fudge))) - t - (goto-char orig) - nil)) - ;; Now, lets find the nearest space (after or before fill column) - (let* ((after (save-excursion - (re-search-forward "[ \t]" nil t))) - (before (save-excursion - (re-search-backward "[ \t]" nil t))) - (afterd (- (or after (matlab-point-at-eol)) (point))) - (befored (- (point) (or before (matlab-point-at-bol))))) - ;; Here, if "before" is actually the beginning of our - ;; indentation, then this is most obviously a bad place to - ;; break our lines. - (if before - (save-excursion - (goto-char before) - (if (<= (point) (save-excursion + (let ((lvl1 (matlab-compute-line-context 1))) + (if (matlab-line-ellipsis-p lvl1) + nil + (save-restriction + (narrow-to-region (matlab-point-at-bol) (matlab-point-at-eol)) + ;; get ourselves onto the fill-column. + (move-to-column fill-column) + (let ((pos nil) + (orig (point))) + (or + ;; Next, if we have a trailing comment, use that. + (progn (setq pos (or (matlab-line-comment-p lvl1) + (matlab-point-at-bol))) + (goto-char pos) + (if (and (> (current-column) (- fill-column matlab-fill-fudge)) + (< (current-column) (+ fill-column matlab-fill-fudge))) + t + (goto-char orig) + nil)) + ;; Now, lets find the nearest space (after or before fill column) + (let* ((after (save-excursion + (re-search-forward "[ \t]" nil t))) + (before (save-excursion + (re-search-backward "[ \t]" nil t))) + (afterd (- (or after (matlab-point-at-eol)) (point))) + (befored (- (point) (or before (matlab-point-at-bol))))) + ;; Here, if "before" is actually the beginning of our + ;; indentation, then this is most obviously a bad place to + ;; break our lines. + (if before + (save-excursion + (goto-char before) + (if (<= (point) (save-excursion + (back-to-indentation) + (point))) + (setq before nil)))) + (cond ((and after + (< afterd matlab-fill-fudge) + (< afterd befored)) + (goto-char after) + t) + ((and before + (< befored matlab-fill-fudge) + (< befored afterd)) + (goto-char before) + t) + (t (goto-char orig) + nil))) + ;; Now, lets find the nearest backwards + (progn + (re-search-backward "\\(\\s-\\|\\s.\\)+" nil t) + (while (and (looking-at "\\^\\|\\.\\|'") + (re-search-backward "\\(\\s-\\|\\s.\\)+" nil t))) + (if (or (not (looking-at "\\(\\s-\\|\\s.\\)+")) + (<= (point) (save-excursion (back-to-indentation) - (point))) - (setq before nil)))) - (cond ((and after - (< afterd matlab-fill-fudge) - (< afterd befored)) - (goto-char after) - t) - ((and before - (< befored matlab-fill-fudge) - (< befored afterd)) - (goto-char before) - t) - (t (goto-char orig) - nil))) - ;; Now, lets find the nearest backwards - (progn - (re-search-backward "\\(\\s-\\|\\s.\\)+" nil t) - (while (and (looking-at "\\^\\|\\.\\|'") - (re-search-backward "\\(\\s-\\|\\s.\\)+" nil t))) - (if (or (not (looking-at "\\(\\s-\\|\\s.\\)+")) - (<= (point) (save-excursion - (back-to-indentation) - (point)))) - (progn - ;; We failed in our mission to find anything, or fell - ;; of the edge of the earth. If we are out of - ;; bounds, lets try again. - (goto-char orig) - (if (re-search-backward "\\s.+" nil t) - t - nil)) - ;; Ok, we have a good location to break. Check for column - ;; and ref against nearest list ending to predict a possibly - ;; better break point. - (forward-char 1) - (let ((okpos (current-column)) - (startlst (save-excursion + (point)))) + (progn + ;; We failed in our mission to find anything, or fell + ;; of the edge of the earth. If we are out of + ;; bounds, lets try again. + (goto-char orig) + (if (re-search-backward "\\s.+" nil t) + t + nil)) + ;; Ok, we have a good location to break. Check for column + ;; and ref against nearest list ending to predict a possibly + ;; better break point. + (forward-char 1) + (let ((okpos (current-column)) + (startlst (save-excursion + (condition-case nil + (matlab-up-list -1) + (error nil)) + (if (save-excursion + (forward-char -1) + (looking-at "\\w")) + (forward-word -1)) + (current-column))) + (endlst (save-excursion (condition-case nil - (matlab-up-list -1) + (matlab-up-list 1) (error nil)) - (if (save-excursion - (forward-char -1) - (looking-at "\\w")) - (forward-word -1)) - (current-column))) - (endlst (save-excursion - (condition-case nil - (matlab-up-list 1) - (error nil)) - (current-column)))) - ;; When evaluating list fudge factors, breaking on the - ;; edge of a list, or at the beginning of a function - ;; call can be more valuable than breaking on a symbol - ;; of a mid-sized list. As such, allow double-fudge - ;; for lists. - (cond - ;; First, pick the end of a list. - ((and (< endlst matlab-fill-fudge-hard-maximum) - (<= endlst (+ fill-column matlab-fill-fudge)) - (or (<= (* matlab-fill-fudge 2) (- endlst okpos)) - (<= endlst fill-column)) - (save-excursion - (move-to-column endlst) - (not (looking-at "\\^")))) - (move-to-column endlst) - t) - ;; Else, back up over this list and poke around - ((>= (* 2 matlab-fill-fudge) (- okpos startlst)) - (move-to-column startlst) - t) - ;; Oh well, just do this symbol. - (t (move-to-column okpos) - t))))) - ;; Well, this just sucks - (progn (goto-char orig) - nil)))))) + (current-column)))) + ;; When evaluating list fudge factors, breaking on the + ;; edge of a list, or at the beginning of a function + ;; call can be more valuable than breaking on a symbol + ;; of a mid-sized list. As such, allow double-fudge + ;; for lists. + (cond + ;; First, pick the end of a list. + ((and (< endlst matlab-fill-fudge-hard-maximum) + (<= endlst (+ fill-column matlab-fill-fudge)) + (or (<= (* matlab-fill-fudge 2) (- endlst okpos)) + (<= endlst fill-column)) + (save-excursion + (move-to-column endlst) + (not (looking-at "\\^")))) + (move-to-column endlst) + t) + ;; Else, back up over this list and poke around + ((>= (* 2 matlab-fill-fudge) (- okpos startlst)) + (move-to-column startlst) + t) + ;; Oh well, just do this symbol. + (t (move-to-column okpos) + t))))) + ;; Well, this just sucks + (progn (goto-char orig) + nil))))))) (defun matlab-auto-fill () "Do auto filling. @@ -3557,22 +2534,23 @@ filling which will automatically insert `...' and the end of a line." (move-to-column fill-column) (if (not (bobp)) (forward-char -1)) - (if (matlab-cursor-in-string 'incomplete) + (if (matlab-cursor-in-string) 4 3)) - 0)))) + 0))) + (lvl1 (matlab-compute-line-context 1))) (if (> (current-column) fill-column) (cond - ((matlab-ltype-comm-ignore) + ((matlab-line-comment-ignore-p lvl1) nil) - ((or (matlab-ltype-comm) + ((or (matlab-line-comment-p lvl1) (and (save-excursion (move-to-column fill-column) (matlab-cursor-in-comment)) - (matlab-lattr-comm))) + (matlab-line-comment-p lvl1))) ;; If the whole line is a comment, do this. (matlab-set-comm-fill-prefix) (do-auto-fill) (matlab-reset-fill-prefix)) - ((and (matlab-ltype-code) - (not (matlab-lattr-cont)) + ((and (not (or (matlab-line-comment-p lvl1) (matlab-line-empty-p lvl1))) + (not (matlab-line-ellipsis-p lvl1)) matlab-fill-code) ;; If we are on a code line, we ellipsify before we fill. (let ((m (make-marker))) @@ -3582,7 +2560,7 @@ filling which will automatically insert `...' and the end of a line." nil (if (not (save-excursion (forward-char -1) - (matlab-cursor-in-string 'incomplete))) + (matlab-cursor-in-string))) (progn (delete-horizontal-space) (insert " " matlab-elipsis-string "\n") @@ -3668,6 +2646,9 @@ Non-nil JUSTIFY-FLAG means justify comment lines as well." (forward-line 1) (beginning-of-line)))) +(defconst matlab-cline-start-skip "[ \t]*%[ \t]*" + "*The regular expression for skipping comment start.") + (defun matlab-fill-comment-line (&optional justify) "Fill the current comment line. With optional argument, JUSTIFY the comment as well." @@ -3715,23 +2696,21 @@ With optional argument, JUSTIFY the comment as well." Paragraphs are always assumed to be in a comment. ARG is passed to `fill-paragraph' and will justify the text." (interactive "P") - (cond ((or (matlab-ltype-comm) + (cond ((or (matlab-line-comment-p (matlab-compute-line-context 1)) (and (matlab-cursor-in-comment) - (not (matlab-lattr-cont)))) + (not (matlab-line-ellipsis-p (matlab-compute-line-context 1))))) ;; We are in a comment, lets fill the paragraph with some ;; nice regular expressions. ;; Cell start/end markers of %% also separate paragraphs (let ((paragraph-separate "%%\\|%[a-zA-Z]\\|%[ \t]*$\\|[ \t]*$") (paragraph-start "%[a-zA-Z]\\|%[ \t]*$\\|[ \t]*$\\|%\\s-*\\*") (paragraph-ignore-fill-prefix nil) - (start (save-excursion (matlab-beginning-of-command) + (start (save-excursion (matlab-scan-beginning-of-command) (if (looking-at "%%") (progn (end-of-line) (forward-char 1))) - (beginning-of-line) - (point))) - (end (save-excursion (matlab-end-of-command) - (point))) + (point-at-bol))) + (end (save-excursion (matlab-scan-end-of-command))) (fill-prefix nil)) (matlab-set-comm-fill-prefix) (save-restriction @@ -3739,24 +2718,24 @@ ARG is passed to `fill-paragraph' and will justify the text." ;; a buffer. (narrow-to-region start (min (point-max) (+ end 1))) (fill-paragraph arg)))) - ((matlab-ltype-code) + ((let ((lvl (matlab-compute-line-context 1))) + (not (or (matlab-line-comment-p lvl) (matlab-line-empty-p lvl)))) ;; Ok, lets get the outer bounds of this command, then ;; completely refill it using the smart line breaking code. (save-restriction (narrow-to-region (save-excursion - (matlab-beginning-of-command) + (matlab-scan-beginning-of-command) (beginning-of-line) (point)) (save-excursion - (matlab-end-of-command) - (point))) + (matlab-scan-end-of-command))) ;; Remove all line breaks (goto-char (point-min)) (while (and (re-search-forward "$" nil t) (not (eobp))) (delete-horizontal-space) ;; Blow away continuation marks - (if (matlab-lattr-cont) + (if (matlab-line-ellipsis-p (matlab-compute-line-context 1)) (progn (goto-char (match-beginning 0)) (forward-char 1) @@ -3790,24 +2769,6 @@ ARG is passed to `fill-paragraph' and will justify the text." (t (message "Paragraph Fill not supported in this context.")))) -(defvar gud-matlab-debug-active nil) -(declare-function matlab-shell-gud-minor-mode "matlab-shell-gud") - -(defun matlab-toggle-read-only (&optional arg interactive) - "Toggle read-only bit in MATLAB mode. -This looks to see if we are currently debugging, and if so re-enable -our debugging feature. -Optional argument ARG specifies if the read-only mode should be set. -INTERACTIVE is ignored." - (interactive "P") - (if (and (featurep 'matlab-shell-gud) - gud-matlab-debug-active) - ;; The debugging is active, just re-enable debugging read-only-mode - (matlab-shell-gud-minor-mode 1) - ;; Else - it is not - probably doing something else. - (call-interactively 'read-only-mode) - )) - ;;; Show Paren Mode support ================================================== @@ -3815,8 +2776,8 @@ INTERACTIVE is ignored." "Function to assign to `show-paren-data-function'. Highlights parens and if/end type blocks. Returns a list: \(HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH)" - (unless (or (matlab-cursor-in-string-or-comment) ; Only do this if not in a string. - (matlab-ltype-block-comm)) + (unless (matlab-cursor-in-string-or-comment) ; Only do this if not in a string. + (save-match-data (save-excursion (let ((here-beg nil) @@ -3840,7 +2801,7 @@ Returns a list: \(HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH)" ;; These checks are much faster than regexp ;; Step one - check for parens - (cond ((and here-syntax (= (car here-syntax) 4)) ; open paren + (cond ((and here-syntax (= (syntax-class here-syntax) 4)) ; open paren (setq here-beg (point) here-end (1+ (point))) (condition-case err @@ -3850,14 +2811,14 @@ Returns a list: \(HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH)" there-end (point) there-syntax (syntax-after there-beg) there-char (char-after there-beg)) - (when (or (/= (car there-syntax) 5) + (when (or (/= (syntax-class there-syntax) 5) (/= (cdr there-syntax) here-char) (/= (cdr here-syntax) there-char)) ; this part seems optional ;(message "ts = %S hs=%S tc = %d hc = %d" there-syntax here-syntax there-char here-char) (setq mismatch t)) ) (error (setq mismatch t)))) - ((and here-prev-syntax (= (car here-prev-syntax) 5)) + ((and here-prev-syntax (= (syntax-class here-prev-syntax) 5)) (setq here-beg (1- (point)) here-end (point)) (condition-case err @@ -3867,7 +2828,7 @@ Returns a list: \(HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH)" there-beg (point) there-syntax (syntax-after there-beg) there-char (char-after there-beg)) - (when (or (/= (car there-syntax) 4) + (when (or (/= (syntax-class there-syntax) 4) (/= (cdr there-syntax) here-prev-char) (/= (cdr here-prev-syntax) there-char)) ; this part seems optional (setq mismatch t)) @@ -3876,86 +2837,79 @@ Returns a list: \(HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH)" (t ;; Part 2: Are we looking at a block start/end, such as if end; - ;; If we are on On a word character, or just after a + ;; If we are on a word character, or just after a ;; word character move back one symbol. This will let ;; us use the block begin / end matchers to figure ;; out where we are. - (when (and (not (eobp)) (not (bobp)) (= (car here-prev-syntax) 2)) - (forward-symbol -1)) + (let ((startsym (matlab--mk-keyword-node)) + (endsym nil) + ) + (when (matlab--valid-keyword-node startsym) + (goto-char (nth 2 startsym)) - (matlab-navigation-syntax + (condition-case err + (cond + ((eq (car startsym) 'decl) + ;; We are looking at a 'function' start. + ;; Since functions may not have an end, we need + ;; to handle this case special. + (setq here-beg (nth 2 startsym) + here-end (nth 3 startsym)) + (if (matlab--scan-block-forward) + ;; if context is returned, failed to find something + ;; this is a missmatch if fcns don't have end. + (setq mismatch t) + ;; Otherwise, we found something good + (setq endsym (matlab--mk-keyword-node)) + (if (not endsym) + ;; End of buffer on function w/ no end? + (if matlab-functions-have-end + (setq mismatch t) + (setq mismatch nil)) + ;; Found a symbol + (setq there-beg (nth 2 endsym) + there-end (nth 3 endsym) + mismatch nil) + ) )) - (condition-case err - (cond - ((looking-at "function\\>") - ;; We are looking at a 'function' start. Since functions may not have an end, we need - ;; to handle this case special. - (setq here-beg (match-beginning 0) - here-end (match-end 0)) - (matlab-forward-sexp) - (backward-word 1) - (looking-at (concat (matlab-block-end-pre) "\\>")) - (setq there-beg (match-beginning 0) - there-end (match-end 0) - mismatch nil) - ) - ((looking-at (concat (matlab-block-beg-re) "\\>")) - ;; We are at the beginning of a block. Navigate forward to the end - ;; statement. - (setq here-beg (match-beginning 0) - here-end (match-end 0)) - (matlab-forward-sexp) - (backward-word 1) - (looking-at (concat (matlab-block-end-pre) "\\>")) - (setq there-beg (match-beginning 0) - there-end (match-end 0) - mismatch nil) - ) - ((and (looking-at (concat "\\(" (matlab-block-end-pre) "\\)\\>")) - (matlab-valid-end-construct-p)) - ;; We are at the end of a block. Navigate to the beginning - (setq here-beg (match-beginning 0) - here-end (match-end 0)) - (when (matlab-backward-sexp t t) - (looking-at (concat (matlab-block-beg-re) "\\>")) - (setq there-beg (match-beginning 0) - there-end (match-end 0) - mismatch nil) - )) - ((looking-at (concat (matlab-block-mid-re) "\\>")) - ;; We are at a middle-block expression, like "else" or "catch' - ;; Ideally we'd show the beginning and the end, but lets just show - ;; the beginning. - (setq here-beg (match-beginning 0) - here-end (match-end 0)) - (matlab-backward-sexp t) - (looking-at (concat (matlab-block-beg-re) "\\>")) - (setq there-beg (match-beginning 0) - there-end (match-end 0) - mismatch nil) - ) - - ((looking-at (concat (matlab-endless-blocks-re) "\\>")) - ;; We are at a middle-sub-block expression, like "case" - ;; Ideally we'd show the beginning and the end, but lets just show - ;; the beginning. - (setq here-beg (match-beginning 0) - here-end (match-end 0)) - (matlab-backward-sexp t) - (looking-at (concat (matlab-block-beg-re) "\\>")) - (setq there-beg (match-beginning 0) - there-end (match-end 0) - mismatch nil) - ) - - - ;; No block matches, just return nothing. - (t (setq noreturn t)) - ) - ;; An error occurred. Assume 'here-*' is set, and setup mismatch. - (error (setq mismatch t))) + ;; Misc block starts + ((memq (car startsym) '(ctrl args mcos)) + ;; We are at the beginning of a block. Navigate forward to the end + ;; statement. + (setq here-beg (nth 2 startsym) + here-end (nth 3 startsym)) + (if (matlab--scan-block-forward) + (setq mismatch t) + (setq endsym (matlab--mk-keyword-node)) + (if (not endsym) + (setq mismatch t) + ;; Found a symbol + (setq there-beg (nth 2 endsym) + there-end (nth 3 endsym) + mismatch nil)))) + ;; Misc block middles an ends + ((memq (car startsym) '(end mid case)) + ;; We are at the end of a block or on a middle keyword + ;; like else, catch, or case. In all these go to beginning. + (setq here-beg (nth 2 startsym) + here-end (nth 3 startsym)) + (goto-char here-beg) + (if (matlab--scan-block-backward-up) + (setq mismatch t) + (setq endsym (matlab--mk-keyword-node)) + (if (not endsym) + (setq mismatch t) + ;; Found a symbol + (setq there-beg (nth 2 endsym) + there-end (nth 3 endsym) + mismatch nil)))) + ;; No block matches, just return nothing. + (t (setq noreturn t)) + ) + ;; An error occurred. Assume 'here-*' is set, and setup mismatch. + (error (setq mismatch t)))) ))) (if noreturn @@ -3963,173 +2917,7 @@ Returns a list: \(HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH)" (list here-beg here-end there-beg there-end mismatch) )))))) -;;; Block highlighting ======================================================== - -(defvar matlab-block-highlighter-timer nil - "The timer representing the block highlighter.") - -(defun matlab-enable-block-highlighting (&optional arg) - "Start or stop the block highlighter. -Optional ARG is 1 to force enable, and -1 to disable. -If ARG is nil, then highlighting is toggled." - (interactive "P") - (if (not (fboundp 'matlab-run-with-idle-timer)) - (setq matlab-highlight-block-match-flag nil)) - ;; Only do it if it's enabled. - (if (not matlab-highlight-block-match-flag) - nil - ;; Use post command idle hook as a local hook to dissuade too much - ;; cpu time while doing other things. - ;;(make-local-hook 'post-command-hook) - (if (not arg) - (setq arg - (if (member 'matlab-start-block-highlight-timer - post-command-hook) - -1 1))) - (if (> arg 0) - (add-hook 'post-command-hook 'matlab-start-block-highlight-timer nil :local) - (remove-hook 'post-command-hook 'matlab-start-block-highlight-timer :local)))) - -(defvar matlab-block-highlight-overlay nil - "The last highlighted overlay.") -(make-variable-buffer-local 'matlab-block-highlight-overlay) - -(defvar matlab-block-highlight-timer nil - "Last started timer.") -(make-variable-buffer-local 'matlab-block-highlight-timer) - -(defun matlab-start-block-highlight-timer () - "Set up a one-shot timer if we are in MATLAB mode." - (if (eq major-mode 'matlab-mode) - (progn - (if matlab-block-highlight-overlay - (unwind-protect - (matlab-delete-overlay matlab-block-highlight-overlay) - (setq matlab-block-highlight-overlay nil))) - (if matlab-block-highlight-timer - (unwind-protect - (matlab-cancel-timer matlab-block-highlight-timer) - (setq matlab-block-highlight-timer nil))) - (setq matlab-block-highlight-timer - (matlab-run-with-idle-timer - 1 nil 'matlab-highlight-block-match - (current-buffer)))))) - -(defun matlab-highlight-block-match (&optional buff-when-launched) - "Highlight a matching block if available. -BUFF-WHEN-LAUNCHED is the buffer that was active when the timer was set." - (setq matlab-block-highlight-timer nil) - (if (null buff-when-launched) - ;; We were passed a null. This indicates an old version of XEmacs - ;; so just turn the feature off - (setq matlab-highlight-block-match-flag nil) - ;; Only do neat stuff in the same buffer as the one we were - ;; initialized from. - (when (and buff-when-launched - (eq buff-when-launched (current-buffer))) - (let ((inhibit-quit nil) ;turn on G-g - (matlab-scan-on-screen-only t)) - (if matlab-show-periodic-code-details-flag - (matlab-show-line-info)) - (if (not (matlab-cursor-in-string-or-comment)) - (save-excursion - (if (or (bolp) - (looking-at "\\s-") - (save-excursion (forward-char -1) (looking-at "\\s-"))) - nil - (forward-word -1)) - (if (and (looking-at (concat (matlab-block-beg-re) "\\>")) - (not (looking-at "function"))) - (progn - ;; We scan forward... - (matlab-forward-sexp) - (backward-word 1) - (if (not (looking-at matlab-block-end-pre-if)) - nil ;(message "Unterminated block, or end off screen.") - (setq matlab-block-highlight-overlay - (matlab-make-overlay (point) - (progn (forward-word 1) - (point)) - (current-buffer))) - (matlab-overlay-put matlab-block-highlight-overlay - 'face 'matlab-region-face))) - (if (and (looking-at (concat (matlab-block-end-pre) "\\>")) - (not (looking-at "function")) - (matlab-valid-end-construct-p)) - (progn - ;; We scan backward - (forward-word 1) - (condition-case nil - (progn - (matlab-backward-sexp) - (if (not (looking-at (matlab-block-beg-re))) - nil ;(message "Unstarted block at cursor.") - (setq matlab-block-highlight-overlay - (matlab-make-overlay (point) - (progn (forward-word 1) - (point)) - (current-buffer))) - (matlab-overlay-put matlab-block-highlight-overlay - 'face 'matlab-region-face))) - (error (message "Unstarted block at cursor.")))) - ;; do nothing - )))))))) - - -;;; M Block Folding with hideshow ============================================= - -(defun matlab-hideshow-forward-sexp-func (arg) - "Move forward one sexp for hideshow. -Argument ARG specifies the number of blocks to move forward." - (beginning-of-line) - (matlab-forward-sexp arg) - ) - -(defun matlab-hideshow-adjust-beg-func (arg) - "Adjust the beginning of a hideshow block. -Argument ARG to make it happy." - (end-of-line) - (point) - ) - -;; Use this to enable hideshow in MATLAB. -;; It has not been tested by me enough. - -;; REMOVE PUSHNEW FROM THIS LINE -;;(pushnew (list 'matlab-mode -;; (matlab-block-beg-pre) -;; (matlab-block-end-pre) -;; "%" -;; 'matlab-hideshow-forward-sexp-func -;; 'matlab-hideshow-adjust-beg-func -;; ) -;; hs-special-modes-alist :test 'equal) - - -;;; M Code verification & Auto-fix ============================================ - -(defun matlab-mode-verify-fix-file-fn () - "Verify the current buffer from `write-contents-hooks'." - (if matlab-verify-on-save-flag - (matlab-mode-verify-fix-file (> (point-max) - matlab-block-verify-max-buffer-size))) - ;; Always return nil. - nil) - -(defun matlab-mode-verify-fix-file (&optional fast) - "Verify the current buffer satisfies all M things that might be useful. -We will merely loop across a list of verifiers/fixers in -`matlab-mode-verify-fix-functions'. -If optional FAST is non-nil, do not perform usually lengthy checks." - (interactive) - (let ((p (point)) - (l matlab-mode-verify-fix-functions)) - (while l - (funcall (car l) fast) - (setq l (cdr l))) - (goto-char p)) - (if (matlab-called-interactively-p) - (message "Done."))) +;;; M Code verification ============================================ (defun matlab-toggle-show-mlint-warnings () "Toggle `matlab-show-mlint-warnings'." @@ -4160,70 +2948,127 @@ If optional FAST is non-nil, do not perform usually lengthy checks." matlab-show-mlint-warnings) 1 -1))) ; change mlint mode altogether +;;; Verify / Auto-fix ============================================ + +(defun matlab-mode-verify-fix-file-fn () + "Verify the current buffer from `write-contents-hooks'." + (if matlab-verify-on-save-flag + (matlab-mode-verify-fix-file (> (point-max) + matlab-block-verify-max-buffer-size))) + ;; Always return nil. + nil) + +(defun matlab-mode-verify-fix-file (&optional fast) + "Verify the current buffer satisfies all M things that might be useful. +We will merely loop across a list of verifiers/fixers in +`matlab-mode-verify-fix-functions'. +If optional FAST is non-nil, do not perform usually lengthy checks." + (interactive) + (save-excursion + ;; Always re-validate if functions have end. + (matlab-mode-vf-guess-functions-have-end fast) + ;; Loop over the options. + (mapc (lambda (func) (funcall func fast)) + matlab-mode-verify-fix-functions)) + (if (matlab-called-interactively-p) + (message "Done."))) + ;; ;; Add more auto verify/fix functions here! ;; +(defun matlab-mode-vf-guess-functions-have-end (&optional fast) + "Look at the current buffer state and decide determine if functions have end. +If this is already known, no action is taken." + (let ((filetype (matlab-guess-script-type))) + + ;; Lets if if the file if we were in still doesn't know what to do + ;; a bout ends, and and re-assert what we should do. + (cond + ;; If the file is empty of code (from before, or just now) + ;; then optimize out this step. + ((eq filetype 'empty) + ;; If user deleted content, go back into guess mode. + (setq matlab-functions-have-end 'guess) + (matlab-functions-have-end-minor-mode 1) + ) + + ;; If there is just bad syntax somewhere, skip it with a notice. + ((save-excursion (goto-char (point-max)) (matlab-in-list-p)) + (setq matlab-functions-have-end 'guess) + (matlab-functions-have-end-minor-mode 1) + (message "Unterminated list - skipping block check")) + + ;; If we are in guess mode, but user added content, we can + ;; not have a fresh new guess. + ((eq matlab-functions-have-end 'guess) + (let ((guess (matlab-do-functions-have-end-p 'no-navigate))) + (if guess (matlab-functions-have-end-minor-mode 1) + (matlab-functions-have-end-minor-mode -1))) + ) + + ;; If we are in no-end mode, BUT the filetype is wrong, say something. + ((and (not matlab-functions-have-end) (or (eq filetype 'script) (eq filetype 'class))) + (message "Type of file detected no longer matches `matlab-functions-have-end' of nil, assume t.") + (matlab-functions-have-end-minor-mode 1) + (sit-for 1) + ) + + ;; If functions have end but the style changes, re-up the lighter on the minor mode. + ;; note, we can ignore that 'empty == 'guess b/c handled earlier. + ((and matlab-functions-have-end (not (eq matlab-functions-have-end filetype))) + (matlab-functions-have-end-minor-mode 1)) + + ;; If the variable was specified and file is not empty, then do nothing. + ;; TODO - maybe we should force to t for scripts and classes? + + ) ;; end cond + + )) + (defun matlab-mode-vf-functionname (&optional fast) "Verify/Fix the function name of this file. Optional argument FAST is ignored." (matlab-navigation-syntax (goto-char (point-min)) - (while (and (or (matlab-ltype-empty) (matlab-ltype-comm)) - (/= (matlab-point-at-eol) (point-max))) - (forward-line 1)) + (matlab-find-code-line) (let ((func nil) (bn (file-name-sans-extension - (file-name-nondirectory (buffer-file-name))))) - (if (looking-at (matlab-match-function-re)) - ;; The expression above creates too many numeric matches - ;; to apply a known one to our function. We cheat by knowing that - ;; match-end 0 is at the end of the function name. We can then go - ;; backwards, and get the extents we need. Navigation syntax - ;; lets us know that backward-word really covers the word. - (let ((end (match-end 0)) - (begin (progn (goto-char (match-end 0)) - (forward-word -1) - (point)))) - (setq func (buffer-substring-no-properties begin end)) - (if (not (string= func bn)) - (if (not (matlab-mode-highlight-ask - begin end - "Function and file names are different. Fix function name?")) - nil - (goto-char begin) - (delete-region begin end) - (insert bn)))))))) + (file-name-nondirectory (buffer-file-name)))) + (fcn (matlab-line-declaration-name))) + (when (and fcn (eq (car fcn) 'function) + (not (string= (nth 1 fcn) bn))) + (unless (not (matlab-mode-highlight-ask + (nth 2 fcn) (nth 3 fcn) + "Function name and file names are different. Fix function name?")) + (goto-char (nth 2 fcn)) + (delete-region (nth 2 fcn) (nth 3 fcn)) + (insert bn)))))) (defun matlab-mode-vf-classname (&optional fast) "Verify/Fix the class name of this file. Optional argument FAST is ignored." (matlab-navigation-syntax (goto-char (point-min)) - ;; Skip over whitespace. - (while (and (or (matlab-ltype-empty) (matlab-ltype-comm)) - (/= (matlab-point-at-eol) (point-max))) - (forward-line 1)) + (matlab-find-code-line) (let ((class nil) (bn (file-name-sans-extension - (file-name-nondirectory (buffer-file-name))))) - (if (looking-at (matlab-match-classdef-re)) - ;; The name of this class is match 2. - (let ((end (match-end 2)) - (begin (match-beginning 2))) - (setq class (buffer-substring-no-properties begin end)) - (if (not (string= class bn)) - (if (not (matlab-mode-highlight-ask - begin end - "Class name and file names are different. Fix class name?")) - nil - (goto-char begin) - (delete-region begin end) - (insert bn)))))))) + (file-name-nondirectory (buffer-file-name)))) + (class (matlab-line-declaration-name))) + (when (and class (eq (car class) 'classdef) + (not (string= (nth 1 class) bn))) + (unless (not (matlab-mode-highlight-ask + (nth 2 class) (nth 3 class) + "Class name and file names are different. Fix class name?")) + (goto-char (nth 2 class)) + (delete-region (nth 2 class) (nth 3 class)) + (insert bn)))))) (defun matlab-mode-vf-add-ends (&optional fast) "Verify/Fix adding ENDS to functions. Optional argument FAST skips this test in fast mode." - (when (and matlab-functions-have-end (not fast)) + ;; We used to do extra checking here, but now we do + ;; checking in the verifier + (when (not fast) (matlab-mode-vf-block-matches-forward nil t) )) @@ -4236,49 +3081,63 @@ not be needed. Optional argument FAST causes this check to be skipped. Optional argument ADDEND asks to add ends to functions, and is used by `matlab-mode-vf-add-ends'" - (goto-char (point-min)) - (let ((go t) - (expr (concat "\\<\\(" (matlab-block-beg-pre) "\\)\\>")) + (let ((expr nil) + (scanstate nil) + (exit nil) + ;; lets avoid asking questions based on id of this file + ;; and if ends are optional in the first place. + (filetype (matlab-guess-script-type)) ) + + ;; Before checking syntax, lets re-look at the file if we were in + ;; guess mode and re-assert what we should do. + (when (or (eq filetype 'empty) + (save-excursion (goto-char (point-max)) (matlab-in-list-p))) + ;; In a bad state - go fast. + (setq fast t)) + + ;; Navigate our sexp's and make sure we're all good. (matlab-navigation-syntax - (while (and (not fast) go (re-search-forward expr nil t)) - (forward-word -1) ;back over the special word - (let ((s (point)) - e) - (condition-case nil - (if (and (not (matlab-cursor-in-string-or-comment)) - (not (matlab-ltype-block-comm)) - (or matlab-functions-have-end (not (looking-at "function")))) - (progn - (matlab-forward-sexp) - (forward-word -1) - (if (not (looking-at - (concat matlab-block-end-pre-no-if "\\>"))) - (setq go nil))) - (forward-word 1)) - (error (setq go nil))) - (when (not go) - (goto-char s) - (setq e (save-excursion (forward-word 1) (point))) - ;; Try to add an end to the broken block - (if addend - (if (matlab-mode-highlight-ask - s e "Unterminated block. Try to add end?") - (progn + (goto-char (point-min)) + (while (and (not fast) (not exit) + (matlab--scan-next-keyword 'decl (point-max))) + (forward-word -1) + (if (not (setq scanstate (matlab--scan-block-forward))) + ;; This scan returns any leftover unterminated blocks. + ;; If it is empty, we are fine. + (when (eq matlab-functions-have-end 'guess) + (matlab-functions-have-end-minor-mode 1)) + + ;; If we had an error, but none of the above, try to fix? + ;; Get locations out of the found keyword. + (save-excursion + (let ((s (nth 2 (car scanstate))) + (e (nth 3 (car scanstate)))) + (goto-char s) + ;; Try to add an end to the broken block + (if addend + (if (matlab-mode-highlight-ask + s e + "Unterminated block. Try to add end?") (matlab-mode-vf-add-end-to-this-block) - (setq go t)) - ;; Else, mark this buffer as not needing ends. - (setq matlab-functions-have-end nil) - (message "Marking buffer as not needing END for this session.") - (sit-for 1) - ) - ;; We aren't in addend mode then we are in plain verify - ;; mode - (if (matlab-mode-highlight-ask - s e - "Unterminated block. Continue anyway?") - nil ;; continue anyway. - (error "Unterminated Block found!"))))) + ;; Else, mark this buffer as not needing ends, + ;; but ONLY if a function buffer + (when (eq filetype 'function) + (if (matlab-mode-highlight-ask + s e + "Should funtions have end in this file?") + (matlab-functions-have-end-minor-mode 1) + (matlab-functions-have-end-minor-mode -1) + (message "Marking buffer as not needing END for this session.") + (sit-for 1)))) + ;; We aren't in addend mode then we are in plain verify + ;; mode + (if (matlab-mode-highlight-ask + s e + "Unterminated block. Continue anyway?") + nil ;; continue anyway. + (error "Unterminated Block found!"))) + ))) ;; save-excursion, let, if (message "Block-check: %d%%" (/ (/ (* 100 (point)) (point-max)) 2)))))) (defun matlab-mode-vf-add-end-to-this-block () @@ -4296,30 +3155,6 @@ by `matlab-mode-vf-add-ends'" (save-excursion (insert "\nend\n\n")) (matlab-indent-line)))) -(defun matlab-mode-vf-block-matches-backward (&optional fast) - "Verify/fix unstarted (or dangling end) blocks. -Optional argument FAST causes this check to be skipped." - (goto-char (point-max)) - (let ((go t) (expr (concat "\\<\\(" (matlab-block-end-no-function-re) - "\\)\\>"))) - (matlab-navigation-syntax - (while (and (not fast) go (re-search-backward expr nil t)) - (forward-word 1) - (let ((s (point))) - (condition-case nil - (if (and (not (matlab-cursor-in-string-or-comment)) - (matlab-valid-end-construct-p)) - (matlab-backward-sexp) - (backward-word 1)) - (error (setq go nil))) - (if (and (not go) (goto-char s) - (not (matlab-mode-highlight-ask - (point) (save-excursion (backward-word 1) (point)) - "Unstarted block. Continue anyway?"))) - (error "Unstarted Block found!"))) - (message "Block-check: %d%%" - (+ (/ (/ (* 100 (- (point-max) (point))) (point-max)) 2) 50)))))) - ;;; Utility for verify/fix actions if you need to highlight ;; a section of the buffer for the user's approval. (defun matlab-mode-highlight-ask (begin end prompt) @@ -4361,25 +3196,11 @@ desired. Optional argument FAST is not used." (setq msgpos (+ msgpos dir)) (if (or (> msgpos 5) (< msgpos 0)) (setq dir (- dir) msgpos (+ (* 2 dir) msgpos))) - (matlab-end-of-command (point)) - (if (matlab-cursor-in-comment) - (progn - (matlab-comment-on-line) - (skip-chars-backward " \t"))) - (if (and (not (= (preceding-char) ?\;)) - (not (matlab-cursor-in-string t)) - (not (save-excursion - (beginning-of-line) - (looking-at matlab-quiesce-nosemi-regexp)))) - (let ((p (point))) - (skip-chars-backward " \t") - (if (/= p (point)) - (progn - (delete-region p (point)) - (forward-line -1)) - (if (matlab-mode-highlight-ask (point) (+ 1 (point)) - "Add Semi colon here? ") - (insert ";"))))) + (matlab-scan-end-of-command) + (when (and (matlab-line-end-of-code-needs-semicolon-p) + (matlab-mode-highlight-ask (point) (+ 1 (point)) + "Add Semi colon here? ")) + (insert ";")) (forward-line 1)))) (message "Scanning .... done")) @@ -4390,18 +3211,31 @@ desired. Optional argument FAST is not used." (defun matlab-show-line-info () "Display type and attributes of current line. Used in debugging." (interactive) - (let ((msg "line-info:") - (indent (matlab-calculate-indentation (current-indentation))) - (nexti (matlab-next-line-indentation))) - (setq msg (concat msg - " Line type: " (symbol-name (car indent)) - " This Line: " (int-to-string (nth 1 indent)) - " Next Line: " (int-to-string nexti))) - (if (matlab-lattr-cont) + (matlab-navigation-syntax + (let* ((msg "") + (lvl2 (matlab-compute-line-context 2)) + (lvl1 (matlab-get-lvl1-from-lvl2 lvl2)) + (lvl1msg (matlab-describe-line-indent-context lvl1 t)) + (indent nil) + (fullindent (matlab--calc-indent lvl2 'indent)) + (nexti (matlab-next-line-indentation lvl1)) + (defn (matlab-current-defun)) + ) + (setq msg (concat msg + "Line Syntax: " lvl1msg + " | Preferred Indents: This: " (int-to-string (nth 1 indent)) + " Next: " (int-to-string nexti) + " Indent Style: " (symbol-name (car indent)) + )) + + (when (matlab-line-ellipsis-p lvl1) (setq msg (concat msg " w/cont"))) - (if (matlab-lattr-comm) + (when (matlab-line-end-comment-point lvl1) (setq msg (concat msg " w/comm"))) - (message msg))) + (when defn + (setq msg (concat msg " Defun: " defn))) + + (message "%s" msg)))) (provide 'matlab) @@ -4419,10 +3253,10 @@ desired. Optional argument FAST is not used." ;; LocalWords: symbolp prev lst nlst nreverse Aki Vehtari backquote ;; LocalWords: defmacro oldsyntax edebug cline ctxt eobp bobp sc fc ;; LocalWords: udir funcall sexps skipnav eolp autoend noerror returnme -;; LocalWords: Unstarted includeelse autostart lattr zerop cellstart blockcomm +;; LocalWords: Unstarted includeelse autostart lattr zerop cellstart ;; LocalWords: linebounds bol commtype startmove nomove charvector sregex ;; LocalWords: insregex laststart bolp calc ci sem DEPTHNUMBER blockstart -;; LocalWords: blockmid blockendless blockend unstarted listp boc parendepth +;; LocalWords: blockmid blockendless blockend unstarted listp boc parencol ;; LocalWords: cci startpnt hc rc nosemi emacsen afterd befored okpos startlst ;; LocalWords: endlst ellipsify noreturn hs tc matchers hideshow func PUSHNEW ;; LocalWords: pushnew bn nondirectory un msgpos nexti diff --git a/mlint.el b/mlint.el index b1e4c6f..766353e 100644 --- a/mlint.el +++ b/mlint.el @@ -188,8 +188,14 @@ be cause for being turned off in a buffer." ( NOPRT . mlint-lm-quiet ) ( NOSEM . mlint-lm-delete-focus ) ( NOCOM . mlint-lm-delete-focus ) + ( MSNU . mlint-lm-delete-focus ) ( ST2NM . mlint-lm-str2num ) ( FDEPR . mlint-lm-entry-deprecated ) + ( ENDCT . mlint-lm-missing-end ) + ( ENDCT2 . mlint-lm-missing-end ) + ( FNDEF . mlint-lm-function-name ) + ( MCFIL . mlint-lm-function-name ) + ( MCSCC . mlint-lm-function-name ) ) "List of warning IDs and auto-fix functions. If the CAR of an association matches an error id then the linemark entry @@ -468,7 +474,7 @@ ACTIVE-P if it should be made visible." "Return non-nil if entry E can be automatically fixed." (oref-default e fixable-p)) -(cl-defmethod mlint-fix-entry :AFTER ((e mlint-lm-entry)) +(cl-defmethod mlint-fix-entry :after ((e mlint-lm-entry)) "Stuff to do after a warning is considered fixed. Subclasses fulfill the duty of actually fixing the code." (linemark-display e nil) @@ -497,6 +503,8 @@ Subclasses fulfill the duty of actually fixing the code." ) (goto-char s) (delete-region (point) e) + ;; If this happened to be at end of line, just delete all left over whitespace. + (when (looking-at "\\s-*$") (delete-horizontal-space)) (point) )) ) @@ -508,13 +516,13 @@ Subclasses fulfill the duty of actually fixing the code." "Class which can replace the focus area." :abstract t) -(cl-defmethod initialize-instance :AFTER ((this mlint-lm-replace-focus) +(cl-defmethod initialize-instance :after ((this mlint-lm-replace-focus) &rest fields) "Calculate the new fix description for THIS. Optional argument FIELDS are the initialization arguments." ;; After basic initialization, update the fix description. (oset this fix-description - (concat (oref-default mlint-lm-replace-focus fix-description) + (concat (oref-default this fix-description) (oref this new-text)))) (cl-defmethod mlint-fix-entry ((ent mlint-lm-replace-focus)) @@ -533,7 +541,7 @@ Optional argument FIELDS are the initialization arguments." "Entry for anything that is deprecated. Extracts the replacement for the deprecated symbol from the warning message.") -(cl-defmethod initialize-instance :AFTER ((this mlint-lm-entry-deprecated) +(cl-defmethod initialize-instance :after ((this mlint-lm-entry-deprecated) &rest fields) "Calculate the 'new text' for THIS instance. Optional argument FIELDS are the initialization arguments." @@ -544,10 +552,33 @@ Optional argument FIELDS are the initialization arguments." (oset this new-text newfcn) ;; After basic initialization, update the fix description. (oset this fix-description - (concat (oref-default mlint-lm-replace-focus fix-description) + (concat (oref-default this fix-description) newfcn)) )) +(defclass mlint-lm-function-name (mlint-lm-replace-focus) + () + "When function name is missmatched with the file name." + ) + +(cl-defmethod initialize-instance :after ((this mlint-lm-function-name) &rest fields) + "Compute the 'new text' for THIS to be the file name from the message. +Optional arguments FIELDS are the initialization arguments." + (let* ((warn (oref this warning)) + (junk (or (string-match "file name: '\\([a-zA-z][a-zA-z0-9]+\\)'" warn) + (string-match "do not agree: '\\([a-zA-z][a-zA-z0-9]+\\)'" warn) + (string-match "of the subclass '\\([a-zA-z][a-zA-z0-9]+\\)'" warn)) + ) + (newfcn (when junk (match-string 1 warn)))) + (oset this new-text newfcn) + ;; After basic initialization, update the fix description. + (oset this fix-description + (concat (oref-default this fix-description) + newfcn)) + )) + +;;; Custom auto-fix entries +;; (defclass mlint-lm-entry-logicals (mlint-lm-entry) ((fixable-p :initform t) (fix-description :initform "perform a replacement.") @@ -600,6 +631,53 @@ Optional argument FIELDS are the initialization arguments." (insert ";")) ) +(defclass mlint-lm-missing-end (mlint-lm-entry) + ((fixable-p :initform t) + (fix-description :initform "Add matching end for this line.")) + "Missing end with guess as to where it might go." + ) + +(cl-defmethod mlint-fix-entry ((ent mlint-lm-missing-end)) + "Add semi-colon to end of this line." + (save-excursion + (let* ((msg (oref ent warning)) + line blockname) + ;; Extract info about this. + (when (string-match "(after line \\([0-9]+\\))" msg) + (setq line (match-string 1 msg))) + (when (string-match "possibly matching \\([A-Z]+\\)\\." msg) + (setq blockname (match-string 1 msg))) + + ;; Did we get the right kind of warning + (if line + ;; We have a line number, just go for it there. + (progn + (mlint-goto-line (string-to-number line)) + ;; add the end and indent + (indent-region (point) (save-excursion (insert "end\n") (point))) + ) + (if (and blockname (string= blockname "FUNCTION")) + ;; It is a function, but no line number. Let's guess where this end + ;; should go. + (save-excursion + (mlint-goto-line (oref ent line)) ;; go to the fcn + (end-of-line) + (if (re-search-forward "^function " nil t) + (progn + (beginning-of-line) + ;; skip over comments that might be headers to the found function. + (matlab-previous-command-begin + (matlab-compute-line-context 2)) ;;(matlab-find-prev-code-line) + (forward-line 1) + (save-excursion (insert "end\n\n")) + (matlab-indent-line)) + (goto-char (point-max)) + (save-excursion (insert "\nend\n\n")) + (matlab-indent-line)))) + ) + )) + ) + ;;; User functions ;; (defun mlint-highlight (err) @@ -740,10 +818,14 @@ Highlight problems and/or cross-function variables." (if (not n) (message "No warning at point.") (let ((col (matlab-comment-on-line))) - (or col (end-of-line)) - (insert " %#ok") - ;; Add spaces if there was a comment. - (when col (insert " "))) + (if col + (progn + (goto-char col) + (skip-chars-forward "% ") + (insert "#ok ")) + (end-of-line) + (insert " %#ok")) + ) ;; This causes inconsistencies. ;; (linemark-delete n) )) diff --git a/semantic-matlab.el b/semantic-matlab.el index 0cb5565..346bef7 100644 --- a/semantic-matlab.el +++ b/semantic-matlab.el @@ -206,7 +206,7 @@ Return argument is: (while (re-search-forward semantic-matlab-match-methods-block-re nil end) (save-excursion ;; find end of properties block (goto-char (match-beginning 0)) - (matlab-forward-sexp nil nil) + (matlab-forward-sexp nil) (setq tmpend (point))) (setq attrs (semantic-matlab-parse-attributes-and-move)) @@ -239,7 +239,7 @@ Return argument is: (setq attrs (semantic-matlab-parse-attributes-and-move)) (save-excursion ;; find end of properties block - (matlab-forward-sexp nil t) + (matlab-forward-sexp t) (beginning-of-line) (setq tmpend (point))) diff --git a/semanticdb-matlab.el b/semanticdb-matlab.el index 9fe973c..1d51a1a 100644 --- a/semanticdb-matlab.el +++ b/semanticdb-matlab.el @@ -76,7 +76,7 @@ ;; Create the database, and add it to searchable databases for matlab mode. (defvar-mode-local matlab-mode semanticdb-project-system-databases (list - (semanticdb-project-database-matlab "Matlab")) + (make-instance 'semanticdb-project-database-matlab)) "Search MATLAB path for symbols.") ;; NOTE: Be sure to modify this to the best advantage of your @@ -99,7 +99,7 @@ Create one of our special tables that can act as an intermediary." ;; We need to return something since there is always the "master table" ;; The table can then answer file name type questions. (when (not (slot-boundp obj 'tables)) - (let ((newtable (semanticdb-table-matlab "MATLAB system table"))) + (let ((newtable (make-instance 'semanticdb-table-matlab))) (oset obj tables (list newtable)) (oset newtable parent-db obj) (oset newtable tags nil) @@ -337,8 +337,7 @@ Return a list of tags." (let ((files (semanticdb-matlab-find-name regex 'regex))) (delq nil (mapcar #'(lambda (x) - (let ((matlab-vers-on-startup nil)) - (car (semanticdb-file-stream x)))) + (car (semanticdb-file-stream x))) files))))) (cl-defmethod semanticdb-find-tags-for-completion-method @@ -378,8 +377,7 @@ Returns a table of all matching tags." ;; generate tags (delq nil (mapcar #'(lambda (x) - (let ((matlab-vers-on-startup nil)) - (car (semanticdb-file-stream x)))) + (car (semanticdb-file-stream x))) compdb))))) (provide 'semanticdb-matlab) diff --git a/tests/Makefile b/tests/Makefile index 11276a7..7f8ccd4 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -20,8 +20,11 @@ VERSION=4.0 DISTDIR=$(top)matlab-emacs-$(VERSION)/tests - +ifdef OS # No MATLAB shell tests on windows. +all: tests matlab modetests +else # The OS variable is only defined on windows, so linux systems can run shell tests. all: tests matlab modetests shelltests +endif .PHONY: modetests modetests: metest.elc diff --git a/tests/blocks.m b/tests/blocks.m new file mode 100644 index 0000000..317c538 --- /dev/null +++ b/tests/blocks.m @@ -0,0 +1,213 @@ +% >>1 +classdef blocks < handle +% !!0 + +% >>11 + properties %!!4 + normalprop = 1; %!!8 + end % <<11 + + %>>12 + properties(Access='public') %!!4 + + % See if we can create properties using keywords + %properties = 1; + %methods = 1; + %events = 1; + arguments %!!8 + prop = 1; + + end %<<12 + + % >> 13 + events (Access='private') %!!4 + + %properties + %events + %methods + arguments %#ok %!!8 + misc %!!8 + + end % <<13 + + %>>14 + methods %!!4 + + %>>15 + function simple_method(obj) %!!8 + %>>151 + arguments %!!12 + obj %!!16 + end %<<151 + + disp(obj.normalprop); + end %<<15 + + %>>16 + function obj = blocks(arguments,events,properties,methods,enumeration,normal)%!!8 + %>>161 + arguments %!!12 + arguments%!!16 + events%!!16 + properties%!!16 + methods%!!16 + enumeration%!!16 + normal%!!16 + end %<<161 + + obj.prop = arguments;%!!12 + obj.prop = events;%!!12 + obj.prop = properties;%!!12 + obj.prop = methods;%!!12 + obj.prop = enumeration;%!!12 + obj.prop = normal;%!!12 + end %<<16 + + %>>17 + function properties(~)%!!8 + end %<<17 + + %>>18 + function methods(~)%!!8 + end %<<18 + + %>>19 + function events(~)%!!8 + end %<<19 + + %>>20 + function events=arguments(arguments)%!!8 + arguments, arguments(:,:) {mustBeNumeric}, end %!!12 +%^ ^kw ^vn ^ty ^df ^kw ^co + enumeration ... %!!12 + ...%^ ^df + = arguments; +%^ ^df + + if enumeration > 0 %!!12 + arguments = -enumeration; %!!16 +%^ ^df ^bi ^df + end %!!12 + + events ... %!!12 + = arguments + 1; +%^ ^df + + end %<<20 + + %>>21 + function enumeration(~)%!!8 +%^ ^fn + end %<<21 + + function y = multiple_arg_blocks(a, b, varargin) %!!8 + + arguments %!!12 +%^ ^kw + a uint32 %!!16 +%^ ^vn ^ty + b uint32 %!!16 +%^ ^vn ^ty + end %!!12 +%^ ^kw + + arguments (Repeating) %!!12 +%^ ^kw ^ty + varargin %!!16 +%^ ^vn + end %!!12 +%^ ^kw + + y = a+b+length(varargin); %!!12 + + end + + function linLog(x,y,scale) + arguments(Repeating) %!!12 + x (1,:) double + y (1,:) double + end %!!12 + arguments %!!12 + scale.Plottype(1,1) string %!!16 +%^ ^vn ^vn ^ty ^ty + end %!!12 + + sprintf('%d %d %s\n', x, y, scale); + end + + + %>>22 + function usestuff(obj)%!!8 + % Try using the methods of this object + obj.properties();%!!12 + obj.methods();%!!12 + obj. events();%!!12 + obj. arguments();%!!12 + obj. enumeration();%!!12 + normal();%!!12 + end %<<22 + + %>>23 + function [m,s] = blarg(~, arguments)%!!8 + % Starter comments. + + %>>231 + arguments%!!12 + ~%!!16 + arguments(:,:) {mustBeNumeric}%!!16 + end %<<231 + + m = mean( ...%!!12 + arguments, 'all'); %!!16 + + arguments = 1;%!!12 + events = arguments;%!!12 + methods = events;%!!12 + properties = methods;%!!12 + enumeration = properties;%!!12 + + s = enumeration;%!!12 + end %<<23 + + %>>24 + function s = simple(~,arguments)%!!8 + % Simple function + + events = arguments;%!!12 + methods = events;%!!12 + properties = methods;%!!12 + enumeration = properties;%!!12 + + s = enumeration; %!!12 + end %<<24 + + function methods=foo3(obj,properties) %!!8 + methods=obj.arguments(properties); %!!12 + end %!!8 + + function s=struct_stuff(~) %!!8 + + s.if = 1; %!!12 + s.else = 1.5; %!!12 + s.while = 2; %!!12 + s.switch = 3; %!!12 + s.case = 3.1; %!!12 + s.end = 5; %!!12 + + end %!!8 + + function tightcomments(~)%!!8 + if condition%!!12 + switch thing %!!16 + case b %!!18 + end%!!16 + end%!!12 + end%!!8 + + %!!8 + end %<<14 + + %!!4 +end % <<1 + +%!!0 diff --git a/tests/complete.m b/tests/complete.m new file mode 100644 index 0000000..3fd5e80 --- /dev/null +++ b/tests/complete.m @@ -0,0 +1,47 @@ +function complete (a_arg1, b_arg2) +% This function is for testing completion tools + + arguments + a_arg1 (1,1) double + b_arg2 (2,1) int + end + + global a_global + global b_global + + a_localvar = 1; + b_localvar = 1; + + for a_for=1:10 + end + + if bool_var + end + + switch a_switch + end + + a_ + % @@(solo var "a_localvar" "a_switch" "a_for" "a_global" "a_arg1") + % @@(solo fcn ) + + % Note: For b, there are other test files the completion + % engine will find, so they are included. + b + % @@(solo fcn "blocal_fcn" "blazy_fcn" "buggy" "blocks") + % @@(solo var "b_localvar" "bool_var" "b_global" "b_arg2") + + + % quiet mlint + blocal_fcn(a_arg1, b_arg2, a_localvar, b_localvar, a_global, b_global); +end + +function blocal_fcn(varargin) + + blazy_fcn(varargin{:}); + +end + +function blazy_fcn(varargin) +end + diff --git a/tests/continuations.m b/tests/continuations.m new file mode 100644 index 0000000..bd0f9fa --- /dev/null +++ b/tests/continuations.m @@ -0,0 +1,184 @@ +function continuations(a,b) %!!0 +% !!0 Help Comment + + arguments (Repeating) + a (1,1) ... % !!8 + double % !!12 + b (1,1) double { mustBeSomething ... %!!8 + mustBeSomethingElse } %!!25 + end + + global var1, ... % !!4 + var2 % !!8 + + localfcn(a,b); % !!4 + + localfcn(var1, ... % !!4 + var2); % !!13 + + localfcn(a, localfcn(b, localfcn(var1,... %!!4 + var2)... %!37 + )... %!!24 + ); %!!12 + + + code1(), ... + code2(); %!!8 + + % NOTE: Blank space below should cancel the indent effect of ellipsis. + code1() ... + + var1 = 1 + ... %!!4 + ... %!!11 + 2 + ... %!!11 + 3; %!!11 + + medvar = 1 + ... %!!4 + 2; %!!13 + + long_var_name = 1 + ... %!!4 + 2; %!!8 + + localfcn (medvar, ... %!!4 + long_var_name); %!!19 + + localfcn( ... %!!4 + a,b); %!!8 + + + if true, if true, if true %#ok %!!4 + localfcn(a,b); ... %!!16 + end; ... %!!4 + end; ... %!!4 + end ... %!!4 + + odd_if_location(); %!!4 - this after those continued ends + + + ... % !!4 A continuation with a ctrl block after it + for g=1:10 %#ok % !!8 b/c continuation + localfcn(a,g) % !!8 b/c not continued, and +4 from comment continuation + end % !!4 to match + + % !!4 to undo continuation. + + % Indent past 1st arg for special functions + set(myhandle, 'Prop1', value, ... %!!4 + 'Prop2', value, ... %!!18 + 'Prop3', value); %!!18 + + % Indent past = sign for assignments. + A = 1 + ... % !!4 + 2; % !!8 + + medvar = 1 + ... % !!4 + 2; % !!13 + + alongvariablename = 1 +... % !!4 + 2; % !!8 + + + fancyfunctionname(arg1, ... %!!4 + innerfcn(arg2, ... %!!22 + arg3), ... %!!31 + { cell1; %!!22 + cell2; %!!24 + [ 1 2 ; %!!24 + 3 4 ] ; %!!26 + cell 4 },... %!!24 + arg5); %!!22 + + + ... % Continuation by itself just before an end. +end %!!0 + +function [ a, ... !!0 + b, ... !!11 + c, ... !!11 + d ] ... !!11 + = continuations_in_return(opt) +% H1 Comment Line !!0 + + code(); %!! 4 + +end %!! 0 + +function [ a, b ] = continuation_in_args(a,... + b) %!!41 +% H1 comment line !!0 + +end + +function c=expression_cont(a,b) +% H1 line !!0 + + if (a > 0 && ... %!!4 + b < 0) %!!8 + + % comment one !!8 + c=1; %!!8 + elseif (a < 0 && ... %!!4 + b > 0) %!!12 + + % comment two !!8 + c=2; %!!8 + end %!!4 + + switch a %!!4 + case 'ert' %!!6 + b = 1; %!!8 + case {'sim', ... %!!6 + 'normalsim'} %!!12 + b=2; %!!8 + end %!!4 + +end + +function a=odd_if_location(n) %!!0 + + i=1; while i<10, if xfcn(i,n)==0, i=i+1; %!!4 + else, i=20; end; end %!!21 + + if i<20 %!!4 + a=1; %!!8 + else %!!4 + a=0; %!!8 + end %!!4 + + foo(); +end %!!0 + +function val = localfcn(c,d) %!!0 +% !!0 Help Comment + + try fclose( fid ); catch, end %!!4 + + val = c+d; % !!4 + + odd_end_location_from_dspfwiz_load(1); + +end %!!0 + +function [z,o,n]=odd_end_location_from_dspfwiz_load(opts) %!!0 + if opts.zeros, z = 'On'; %!!4 + else, z = 'Off'; end %!!4 + + if opts.ones, o = 'On'; %!!4 + else, o = 'Off'; end %!!4 + + if opts.neg_ones, n = 'On'; %!!4 + else, n = 'Off'; end %!!4 +end %!!0 + +function a=foo +%{ + for !!2 + for !!2 +%} + if true %#ok %!!4 + if true %!!8 + a = 1; %!!12 + end end %#ok %!!8 +end %!!0 + + diff --git a/tests/empty.m b/tests/empty.m new file mode 100644 index 0000000..7313e86 --- /dev/null +++ b/tests/empty.m @@ -0,0 +1,2 @@ +% Empty M file. +% %%%empty guess guess \ No newline at end of file diff --git a/tests/fontlock.m b/tests/fontlock.m index 9d2b9f8..cb5655d 100644 --- a/tests/fontlock.m +++ b/tests/fontlock.m @@ -1,8 +1,36 @@ -function [a, b, c] = fontlock(input1, ... - input2, ... - input3) -% FONTLOCK testing function for Emacs & MATLAB. + +% TEST FILE FOR FONT LOCK SPECIAL WORDS +function fontlock() +%^ ^kw ^fn ^df +%^ ^ig + +% $$$ ignored comment +%^ ^ig + persistent var1 % !!4 +%^ ^kw ^vn ^co + + global var2 % !!4 +%^ ^kw ^vn ^co + + end +%^ ^kw + +function [ var1, var2 ] = local(input1) +%^ ^kw ^vn ^fn ^vn ^df + +end + +function [a, b] = local2(input1,... + input2) +%^ ^vn +end + +% TODO - these are cross function variables, but we turn off mlint in +% tests, so these aren't tested. +function [a, b, c] = localvars(input1, input2, input3) +%^ ^kw ^vn ^fn ^vn ^vn ^vn ^df + nested(input1); q = input2; @@ -16,6 +44,135 @@ function [a, b, c] = fontlock(input1, ... c = r; end - end + +function keywordstuff() + + while true +%^ ^kw ^ma + + for varname=1:10 +%^ ^kw ^vn ^cn + break +%^ ^kw + end + + if varname == 2 +%^ ^kw ^df ^bi ^df + disp(1) + elseif varname==3 +%^ ^kw ^df ^bi + disp(2) + else +%^ ^kw + disp(3) + end +%^ ^kw + + switch varname +%^ ^kw ^cn + case 1 +%^ ^kw ^cn + disp(1) + continue +%^ ^kw + case 2 +%^ ^kw ^cn + disp(2) + otherwise +%^ ^kw + disp('other'); +%^ ^df ^st + return +%^ ^kw + end +%^ ^kw + + try +%^ ^kw + disp(1) + catch +%^ ^kw + disp(2) + end +%^ ^kw + + + end +end + +function dographics(value) + + f = figure; +%^ ^bi + ax = axes(f); +%^ ^bi + + set ( ax, 'property',value) +%^ ^bi ^vn ^st + + s = open_system('foo.mdl'); +%^ ^si ^st + + set_param(s, 'param', value); +%^ ^si ^vn ^st ^df +end + +function dodebug() + + dbstop in dodebug +%^ ^bo ^cd + + dbclear +%^ ^bo + +end + +function mathstuff() + + myvar = eps + pi + nan + ans + i + NaT + true ; +%^ ^df ^ma ^bi ^ma ^bi ^ma ^bi ^ma ^bi ^ma ^bi ^ma ^bi ^ma ^df +end + +function helptest() +% HELPTEXT has fancy fonts in it. +%^ ^cn + +end + +function name_no_args +%^ ^kw ^fn +end + +function retarg = ret_and_name_no_args % comment +%^ ^kw ^vn ^df ^fn ^co +end + +function retarg = args_have_cont (arg_1, ... + arg_2) +%^ ^vn +end + +function [ retarg1, ... + retarg2, ... + retarg3 ] ... + = name_of_fcn (arg1) +%^ ^df ^fn ^vn ^df + +end + +classdef (Abstract) myclass < handle +%^ ^kw ^ty ^fn ^bi ^cn +end + +classdef (Abstract)myclass>8 if foo %#ok if bar %#ok @@ -116,24 +123,126 @@ function B = ends_in_comments_and_strings() % !!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() +function C = block_starts_in_comments_and_strings(varargin) % !!0 C = 0; - if true % if true + if varargin{1} % if true + % !!8 + else % !!4 + + % !!8 end % if true % see previous function % !!4 for x=1:length(C) % !!4 - - % !!8 + if varargin{2} % !!8 + continue % !!12 + end % !!8 + + break % !!8 + % !!14 + + %!!8 end switch foo() %!!4 @@ -149,7 +258,7 @@ function C = block_starts_in_comments_and_strings() try % !!8 catch %!!4 - + % !!8 end @@ -160,10 +269,12 @@ function B = continuations_and_block_comments % !!0 % !!0 - %{ - !!6 - !!6 - %} +%{ + !!2 { } + !!2 +%} + + arg1=1; %{ % !!4 @@ -188,9 +299,9 @@ function B = continuations_and_block_comments 3 4 ]; % !!10 foo(['this is a very long string' ... %!!4 - 'with a continution to do something very exciting'])%!!9 + 'with a continution to do something very exciting']);%!!9 - set(gcf,'Position',[ 1 2 3 4],... !!4 + set(gcf,'Position',[ 1 2 3 4], ... !!4 'Color', 'red'); % !!12 B = A + 1 + 4 ... @@ -200,6 +311,14 @@ function B = continuations_and_block_comments % 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 @@ -208,15 +327,44 @@ function has_nested_fcn plot(1:10); %!!4 - function am_nested_fcn %!!4 + A = 1; + + function am_nested_fcn() %!!4 % help % !!4 - code(); + 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 \ No newline at end of file diff --git a/tests/mclass.m b/tests/mclass.m index d16c286..79d97fb 100644 --- a/tests/mclass.m +++ b/tests/mclass.m @@ -1,30 +1,39 @@ % >>1 classdef (abstract) mclass < handle & matlab.mixin.SetGetExactNames % #7# +%^ ^kw ^ty ^fn ^cn ^bi ^cn ^co % !!0 +% %%% class class class % >>11 properties (Access='public') % #2# - +%^ ^kw ^ty ^st ^co + % !!8 AP = []; % #2# +%^ ^vn ^df ^co AB = 'charvec with space'; % #2# AC = "string with space and ( "; % #2# AD = fun_call(1,2); % #3# AE (1,:) double {mustBePositive} = 1; % #5# +%^ ^vn ^ty ^df ^co end % <<11 % >> 111 properties (AbortSet=true, NonCopyable=true) % #2# +%^ ^kw ^ty ^ma ^ty ^ma ^co % !!8 AF (1,1) char {mustBeMember(AF, {'High','Medium','Low'})} = 'Low'; % #5# +%^ ^vn ^ty ^df ^st ^df ^st ^co AG (1,1) matlab.lang.OnOffSwitchState = 'on'; % #6# +%^ ^vn ^ty ^ty ^st ^co end % <<111 % >> 112 events % !!8 Event1 +%^ ^vn Event2 end % <<112 @@ -34,10 +43,14 @@ classdef (abstract) mclass < handle & matlab.mixin.SetGetExactNames % #7# % >>16 function obj = mclass() + %^ ^kw ^vn ^fn ^df % !!8 obj.AB = obj.AP(1:end); % !!12 + + unusedvar = 1; %#ok +%^ ^df ^pr disp('charvect with if and for words [ in it'); % #2# @@ -46,18 +59,22 @@ classdef (abstract) mclass < handle & matlab.mixin.SetGetExactNames % #7# notify(obj,'Event1',... 'indent test'); + notify(obj, 'Event1', 'indent test'); +%^ ^df ^vn ^st ^st ^df + + % >>17 while obj.AB % #3# disp("while loop going on here ("); % #2# % !!52 - + % !!16 end % <<17 - + error('function mclass in charvec }'); % #2# - + % !!12 end % <<16 @@ -66,7 +83,7 @@ classdef (abstract) mclass < handle & matlab.mixin.SetGetExactNames % #7# methods (Access='public') % >>13 function meth(obj) % #3# - + % >>14 if obj.AP % #3# @@ -74,8 +91,8 @@ classdef (abstract) mclass < handle & matlab.mixin.SetGetExactNames % #7# else - % >>15 - try + % >>15 + try % comment with if, while, parfor words in it. @@ -91,13 +108,52 @@ classdef (abstract) mclass < handle & matlab.mixin.SetGetExactNames % #7# end % <<12 methods (Abstract, Hidden=true) % #2# - + result = abs_func(a,b) % #3# result = other_abs_fun(a,b) % #3# end + methods %!!4 + + function end_separate_line(~) %!!8 + end %!!8 + + function end_same_line(~), end %!!8 + + function after_end_same_line(~), end %!!8 + + end %!!4 + + methods %!!4 + function properties(~) %!!8 + end %!!8 + + function methods(~) %!!8 + end %!!8 + + function events(~) %!!8 + end %!!8 + + function arguments(~) %!!8 + end %!!8 + + function enumeration(~) %!!8 + end %!!8 + + function usestuff(obj) %!!8 + % Try using the methods of this object + obj.properties(); %!!12 +%^ ^df ^df ^co + obj.methods(); %!!12 + obj.events(); %!!12 + obj.arguments(); %!!12 + obj.enumeration(); %!!12 + end %!!8 + + end %!!4 + end % <<1 % End \ No newline at end of file diff --git a/tests/mclass_cont.m b/tests/mclass_cont.m new file mode 100644 index 0000000..ee0a0cb --- /dev/null +++ b/tests/mclass_cont.m @@ -0,0 +1,8 @@ +classdef mclass_cont < otherThing & ... % !!0 This continuation can case issue for next prop block + handle %!!8 + + properties (Dependent, SetAccess = private) %!!4 + TopicName %!!8 + end %!!4 + +end %!!0 diff --git a/tests/metest.el b/tests/metest.el index 3e4920b..d60a9ca 100644 --- a/tests/metest.el +++ b/tests/metest.el @@ -34,6 +34,7 @@ (require 'matlab-load) (require 'matlab) (require 'cedet-matlab) +(require 'matlab-complete) (require 'semantic-matlab) ;; Enable semantic @@ -42,68 +43,224 @@ (defun metest-all-syntax-tests () "Run all the syntax tests in this file." - (metest-comment-string-syntax-test) - (metest-sexp-counting-test) - (metest-sexp-traversal-test) - (metest-indents-test) - (metest-parse-test) + (setq debug-on-error t) + (matlab-scan-stat-reset ) ;; Enable scanner statistics logging. + + (metest-log-init) + + (setq-default matlab-indent-function-body 'guess) ;; Force the guess system to be exercised. + (metest-run 'metest-end-detect-test) + (setq-default matlab-indent-function-body 'MathWorks-Standard) ;; put it back + + (metest-run 'metest-comment-string-syntax-test) + (metest-run 'metest-fontlock-test) + (metest-run 'metest-sexp-counting-test) + (metest-run 'metest-sexp-traversal-test) + + ;; Randomize indentation first before indenting + ;; to force the indenter to make changes and give + ;; the cahce and performance a harder problem. + (metest-indents-randomize-files) + (metest-run 'metest-indents-test) + + ;; Parsing and completion are high level tools + (metest-run 'metest-parse-test) + (metest-run 'metest-complete-test) + + (metest-log-report (metest-log-write)) + + (matlab-scan-stats-print) ) +(defun metest-run (test) + "Run and time TEST." + (let* ((config (symbol-value test)) + (name (if (stringp config) config (car config))) + (files (or (cdr-safe config) '(""))) + (strlen (apply 'max (mapcar 'length files)))) + (message ">> Starting %s loop on %S" name files) + (dolist (F files) + (princ (format (concat "<< %s %-" (number-to-string strlen) "s ") name F) 'external-debugging-output) + (let ((old debug-on-error) + (out (progn (setq debug-on-error nil) + (metest-timeit test F)))) + (setq debug-on-error old) + (when (listp out) + (princ (format "passed: %s %.2f s\n" (cdr out) (car out)) 'external-debugging-output) + ) + )) + (message ""))) + +(defvar metest-test-error nil) +(defmacro metest-condition-case-error-msg (&rest forms) + "Run FORMS, capturing any errors and associating with (point)." + (declare (indent 0) (debug t)) + `(condition-case err + ,@forms + (error (cond (metest-test-error (error (car (cdr err)))) + (t (metest-error "Lisp: %s" (error-message-string err)))) + 0) + )) + +(defvar met-end-detect-files '("empty.m" "stringtest.m" "mfuncnoend.m" "mfuncnoendblock.m" "mfuncends.m" "mclass.m" "mfuncspacey.m" "mfuncnoendindent.m" "mfuncnofuncindent.m") + "List of files for running end detection tests on.") + +(defvar metest-end-detect-test (cons "END detection" met-end-detect-files)) +(defun metest-end-detect-test (F) + "Run a test to make sure we correctly detect the state of managing 'end'." + (let ((buf (metest-find-file F)) + (ret nil) + (cnt 0)) + (with-current-buffer buf + (goto-char (point-min)) + ;;(message ">> Checking END detection in %S" (current-buffer)) + (if (re-search-forward "%%%\\s-*\\(\\w+\\)\\s-+\\(\\w+\\)\\s-+\\(\\w+\\)$" nil t) + (let ((st-expect (intern (match-string-no-properties 1))) + (end-expect (intern (match-string-no-properties 2))) + (indent-expect (intern (match-string-no-properties 3))) + (st-actual (matlab-guess-script-type)) + (end-actual (matlab-do-functions-have-end-p)) + (indent-actual (matlab-indent-function-body-p)) + ) + (unless (eq st-actual st-expect) + (metest-error "Script type detection failure: Expected %s but found %s" + st-expect st-actual)) + (unless (eq end-actual end-expect) + (metest-error "Script end detection failure: Expected %s but found %s" + end-expect end-actual)) + (unless (eq indent-actual indent-expect) + (metest-error "Script indent detection failure: Expected %s but found %s" + indent-expect indent-actual)) + + (setq ret (list "script[" st-actual "] end[" end-actual "] indent-p[" indent-actual "]")) + ;;(message "<< Script type and end detection passed: %s, %s" st-actual end-actual) + ) + ;; No expected values found in the file. + (metest-error "Test file did not include expected script-type cookie") + )) + ret)) + (defvar met-stringtest-files '("stringtest.m") "List of files for running string tests on.") -(defun metest-comment-string-syntax-test () +(defvar metest-comment-string-syntax-test (cons "string/comment detection" met-stringtest-files)) +(defun metest-comment-string-syntax-test (F) "Run a test to make sure string nd comment highlighting work." - (dolist (F met-stringtest-files) - (let ((buf (find-file-noselect (expand-file-name F met-testfile-path))) - (cnt 0)) + (let ((buf (metest-find-file F)) + (cnt 0) + (noninteractive nil) ;; fake out font lock + ) (with-current-buffer buf (goto-char (point-min)) - (message ">> Starting search loop in %S" (current-buffer)) - (while (re-search-forward "#\\([csveb]\\)#" nil t) - (goto-char (match-end 1)) - (let ((md (match-data)) - (mc (match-string 1)) - (bc (matlab-ltype-block-comm)) - (qd (matlab-cursor-comment-string-context))) + + (let ((md (match-data))) + ;; Force font lock to throw catchable errors. + (font-lock-mode 1) + (font-lock-flush (point-min) (point-max)) + (font-lock-ensure (point-min) (point-max)) + (font-lock-fontify-region (point-min) (point-max)) + + ;; FL test 1: make sure font lock is on and match data didn't change. + (unless font-lock-mode + (metest-error "Font Lock failed to turn on.")) + ;;(unless (equal md (match-data)) + ;; (metest-error "Font Locking transmuted the match data")) + (when (not (get-text-property 2 'fontified)) + (metest-error "Font Lock Failure: can't run test because font lock failed to fontify region.")) + ) + + + ;;(message ">> Starting string/comment detect loop in %S" (current-buffer)) + (while (re-search-forward "#\\([cCisSvVebdr]\\)#" nil t) + (let* ((md (match-data)) + (pt (match-end 1)) + (mc (match-string-no-properties 1)) + (fnt (get-text-property pt 'face)) + (lv1 (matlab-compute-line-context 1)) + (bc (metest-condition-case-error-msg (matlab-line-block-comment-start lv1))) + (qd (metest-condition-case-error-msg (matlab-cursor-comment-string-context))) + ) + (goto-char pt) + ;; Test 1 - what are we? - (unless (or (and (string= "b" mc) bc) + (unless (or (and (string= "b" mc) (and bc (eq 'comment qd))) (and (string= "v" mc) (eq 'charvector qd)) + (and (string= "V" mc) (eq 'charvector qd)) (and (string= "s" mc) (eq 'string qd)) + (and (string= "S" mc) (eq 'string qd)) (and (string= "c" mc) (eq 'comment qd)) - (and (string= "e" mc) (eq 'elipsis qd)) + (and (string= "C" mc) (eq 'comment qd)) + (and (string= "i" mc) (eq 'comment qd)) + (and (string= "e" mc) (eq 'ellipsis qd)) + (and (string= "d" mc) (eq 'commanddual qd)) + (and (string= "r" mc) (eq nil qd)) ) - (error "Syntax Test Failure @ line %d: Expected %s but found %S" - (line-number-at-pos) - (cond ((string= mc "b") "block comment") - ((string= mc "v") "charvector") - ((string= mc "s") "string") - ((string= mc "c") "comment") - ((string= mc "e") "elipsis") - (t "unknown test token")) - qd)) + (metest-error "Syntax Test Failure @ char %d: Expected %s but found %S" + pt + (cond ((string= mc "b") "block comment") + ((string= mc "v") "charvector") + ((string= mc "V") "charvector") + ((string= mc "s") "string") + ((string= mc "S") "string") + ((string= mc "c") "comment") + ((string= mc "C") "comment") + ((string= mc "i") "comment") + ((string= mc "e") "ellipsis") + ((string= mc "d") "commanddual") + ((string= mc "r") "normal code") + (t "unknown test token")) + qd)) ;; Test 2 - is match-data unchanged? (unless (equal md (match-data)) - (error "Syntax checking transmuted the match data")) + (metest-error "Syntax checking transmuted the match data")) + + ;; FL test 2 - Is the matched location fontified correctly? + (when (consp fnt) (setq fnt (car fnt))) + (unless (or (and (string= "b" mc) (eq fnt 'font-lock-comment-face)) + (and (string= "v" mc) (eq fnt 'font-lock-string-face)) + (and (string= "V" mc) (eq fnt 'matlab-unterminated-string-face)) + (and (string= "s" mc) (eq fnt 'font-lock-string-face)) + (and (string= "S" mc) (eq fnt 'matlab-unterminated-string-face)) + (and (string= "c" mc) (eq fnt 'font-lock-comment-face)) + (and (string= "C" mc) (eq fnt 'matlab-cellbreak-face)) + (and (string= "i" mc) (eq fnt 'matlab-ignored-comment-face)) + (and (string= "e" mc) (eq fnt 'font-lock-comment-face)) + (and (string= "d" mc) (eq fnt 'matlab-commanddual-string-face)) + (and (string= "r" mc) (eq fnt nil)) + ) + (metest-error "Font Lock Failure @ char %d: Expected %s but found %S" + pt + (cond ((string= mc "b") "comment face") + ((string= mc "v") "string face") + ((string= mc "V") "unterminated string face") + ((string= mc "s") "string face") + ((string= mc "S") "unterminated string face") + ((string= mc "c") "comment face") + ((string= mc "C") "cellbreak face") + ((string= mc "i") "ignored comment face") + ((string= mc "e") "comment face") + ((string= mc "d") "commanddual string face") + ((string= mc "r") "regular code / no face") + (t "unknown test token")) + (get-text-property pt 'face))) ;; Track (setq cnt (1+ cnt)) )) (kill-buffer buf)) - (message "<< Comment and string syntax test: %d points passed" cnt) - )) - (message "")) + + (list cnt "tests"))) -(defvar met-sexptest-files '("expressions.m" "mclass.m") +(defvar met-sexptest-files '("expressions.m" "mclass.m" "blocks.m") "List of files for running syntactic expression tests.") -(defun metest-sexp-counting-test () - "Run a test to make sure string nd comment highlighting work." - (dolist (F met-sexptest-files) - (let ((buf (find-file-noselect (expand-file-name F met-testfile-path))) +(defvar metest-sexp-counting-test (cons "sexp counting" met-sexptest-files)) +(defun metest-sexp-counting-test (F) + "Run a test to make sure string and comment highlighting work." + (let ((buf (metest-find-file F)) (cnt 0)) (with-current-buffer buf (goto-char (point-min)) - (message ">> Starting sexp counting loop in %S" (current-buffer)) + ;;(message ">> Starting sexp counting loop in %S" (current-buffer)) (while (re-search-forward "#\\([0-9]\\)#" nil t) (save-excursion (goto-char (match-beginning 0)) @@ -111,102 +268,122 @@ (let* ((num (string-to-number (match-string 1)))) (save-restriction (narrow-to-region (point-at-bol) (point)) - (matlab-move-simple-sexp-internal (- num)) + (metest-condition-case-error-msg + (matlab-move-simple-sexp-internal (- num))) (skip-chars-backward " \t;.=%") (if (not (eq (point) (point-min))) (save-restriction (widen) - (error "error at %d: Backward Sexp miscount tried %d, point %d, min %d" - (line-number-at-pos) - num (point) (point-at-bol)))) + (metest-error "Backward Sexp miscount tried %d, point %d, min %d" + num (point) (point-at-bol)))) (skip-chars-forward " \t;.=%") (matlab-move-simple-sexp-internal num) (skip-chars-forward " \t\n;.=%") (if (not (eq (point) (point-max))) (save-restriction (widen) - (error "Error at %d: Forward Sexp miscount tried %d, point %d, dest %d" - (line-number-at-pos) - num (point) (point-at-eol))))) + (metest-error "Forward Sexp miscount tried %d, point %d, dest %d" + num (point) (point-at-eol))))) )) (end-of-line) (setq cnt (1+ cnt)))) (kill-buffer buf) - (message "<< Sexp counting syntax test: %d points passed" cnt) - )) - (message "")) + (list cnt "tests"))) -(defun metest-sexp-traversal-test () +(defvar metest-sexp-traversal-test (cons "sexp block traversal" met-sexptest-files)) +(defun metest-sexp-traversal-test (F) "Run a test to make sure high level block navigation works." - (dolist (F met-sexptest-files) - (let ((buf (find-file-noselect (expand-file-name F met-testfile-path))) + (let ((buf (metest-find-file F)) (cnt 0)) (with-current-buffer buf (goto-char (point-min)) - (message ">> Starting sexp traversal loop in %S" (current-buffer)) + ;;(message ">> Starting sexp traversal loop in %S" (current-buffer)) (while (re-search-forward ">>\\([0-9]+\\)" nil t) (let* ((num (string-to-number (match-string 1))) (num2 0) (begin nil)) (skip-chars-forward " \n\t;%") (setq begin (point)) - (matlab-forward-sexp) - (skip-chars-forward " \n\t;%") - (if (not (looking-at "<<\\([0-9]+\\)")) - (error "Error at %d: Failed to find matching test end token for %d" - (line-number-at-pos) num) - (setq num2 (string-to-number (match-string 1))) - (when (/= num num2) - (error "Error at %d: Failed to match correct test token. Start is %d, end is %d" - (line-number-at-pos) num num2))) - (matlab-backward-sexp) + (metest-condition-case-error-msg (matlab--scan-block-forward)) + (save-excursion + (skip-chars-forward " \n\t;%") + (if (not (looking-at "<<\\([0-9]+\\)")) + (metest-error "Failed to find matching test end token for %d" + num) + (setq num2 (string-to-number (match-string 1))) + (when (/= num num2) + (metest-error "Failed to match correct test token. Start is %d, end is %d" + num num2)))) + (metest-condition-case-error-msg (matlab--scan-block-backward)) (when (/= (point) begin) - (error "Error at %d: Failed to reverse navigate sexp for %d" - (line-number-at-pos) num)) + (metest-error "Failed to reverse navigate sexp for %d" + num)) ) (end-of-line) (setq cnt (1+ cnt)))) (kill-buffer buf) - (message "<< Sexp counting syntax test: %d points passed" cnt) - )) - (message "")) + (list cnt "test"))) -(defvar met-indents-files '("indents.m" "mclass.m") +(defvar met-indents-files '("indents.m" "continuations.m" "mclass.m" "blocks.m" "mfuncends.m" "mfuncnoendblock.m" "mclass_cont.m" "mfuncnofuncindent.m") "List of files for running syntactic indentation tests.") -(defun metest-indents-test () - "Run a test to make sure high level block navigation works." - (dolist (F met-indents-files) - (let ((buf (find-file-noselect (expand-file-name F met-testfile-path))) - (cnt 0)) - (with-current-buffer buf +(defun metest-indents-randomize-files () + "Randomize the indentation in the inents test files." + (interactive) + (message "<< Flattening indentation ...") + (let ((matlab-scan-temporal-cache nil)) ;; disable cache for file load + (dolist (F met-indents-files) + (with-current-buffer (metest-find-file F) (goto-char (point-min)) - ;; (indent-region (point-min) (point-max)) - (message ">> Starting indents loop in %S" (current-buffer)) - (while (re-search-forward "!!\\([0-9]+\\)" nil t) - (let* ((num (string-to-number (match-string 1))) - (calc (matlab-calc-indent)) - (begin nil)) - (when (not (= num calc)) - (error "Error at %d: Indentation found is %d, expected %d" - (line-number-at-pos) calc num)) - ) - (end-of-line) - (setq cnt (1+ cnt)))) - (kill-buffer buf) - (message "<< Indentation syntax test: %d points passed" cnt) - )) - (message "")) + (while (not (eobp)) + (beginning-of-line) + (if (looking-at "^\\s-*$") + (matlab--change-indentation 0) + (matlab--change-indentation 3)) ;;(random 13)? + (forward-line 1) + ) + ;; And don't delete - leave it to find for the next test. + ;; but we do want to restart the mode and force a re-guess of the file type. + (matlab-mode) + )))) + +(defvar metest-indents-test (cons "indenting" met-indents-files)) +(defvar metest-indent-counts 0) +(defun metest-indents-test (F) + "Run a test to make sure high level block navigation works." + (with-current-buffer (metest-find-file F) + (goto-char (point-min)) + (let ((metest-indent-counts 0) + (matlab--change-indentation-override #'metest-indents-test-hook-fcn)) + (metest-condition-case-error-msg + (matlab-indent-region (point-min) (point-max) nil t)) + (kill-buffer (current-buffer)) + (list metest-indent-counts "tests")))) + +(defun metest-indents-test-hook-fcn (indent) + "Hook fcn used to capture indents from `indent-region'." + (save-excursion + (beginning-of-line) + + (when (re-search-forward "!!\\([0-9]+\\)" (point-at-eol) t) + (let ((num (string-to-number (match-string 1)))) + (setq metest-indent-counts (1+ metest-indent-counts)) + (when (not (eq num indent)) + (metest-error "Indentation computed is %s, expected %s" + indent num)))) + + ;; Now do the indent in case a bad indent will trigger a bug later. + (matlab--change-indentation indent) + )) (defvar met-parser-files '("mpclass.m") "List of files for running semantic parsing tests.") -(defun metest-parse-test () +(defvar metest-parse-test (cons "semantic parser" met-parser-files)) +(defun metest-parse-test (F) "Run the semantic parsing test to make sure the parse works." - - (dolist (F met-parser-files) - (let ((buf (find-file-noselect (expand-file-name F met-testfile-path))) + (let ((buf (metest-find-file F)) exp act (cnt 0)) (with-current-buffer buf @@ -217,12 +394,12 @@ ;; Do the test (goto-char (point-min)) - (message ">> Starting semantic parser test in %S" (current-buffer)) + ;;(message ">> Starting semantic parser test in %S" (current-buffer)) (unless (re-search-forward "^%%\\s-*>>\\s-+SEMANTIC TEST" nil t) - (error "Semantic parser test: Failed to find test cookie.")) + (metest-error "Semantic parser test: Failed to find test cookie.")) (unless (re-search-forward "^%{[ \t\n]+\\(((\\)" nil t) - (error "Semantic parser test: Failed to find expected values.")) + (metest-error "Semantic parser test: Failed to find expected values.")) (goto-char (match-beginning 1)) (setq exp (read (buffer-substring (point) (save-excursion (re-search-forward "%}" nil t) @@ -232,25 +409,262 @@ ;; Compare the two lists ... simply. (while (and exp act) (unless (metest-compare-tags (car exp) (car act)) - (error "Expected tag %s, found %s" (semantic-format-tag-prototype (car exp)) - (semantic-format-tag-prototype (car act)))) + (metest-error "Expected tag %s, found %s" (semantic-format-tag-prototype (car exp)) + (semantic-format-tag-prototype (car act)))) (setq exp (cdr exp) act (cdr act) cnt (1+ cnt)) ) (when (or exp act) - (error "Found tags and expected tag lists differnet lengths.\nExpected Remains: %S\nActual Remains: %S" + (metest-error "Found tags and expected tag lists differnet lengths.\nExpected Remains: %S\nActual Remains: %S" exp act)) ) - - (message ">> Semantic parser test: %d tags matched" cnt)))) + (list cnt "tests"))) (defun metest-compare-tags (EXP ACT) "Return non-nil if EXP tag is similiar to ACT" (semantic-tag-similar-p EXP ACT :documentation) - ) +(defconst met-kw-font-alist '(( "kw" . font-lock-keyword-face ) + ( "ty" . font-lock-type-face ) + ( "fn" . font-lock-function-name-face ) + ( "vn" . font-lock-variable-name-face ) + ( "vc" . (font-lock-variable-name-face + matlab-cross-function-variable-face) ) + ( "cn" . font-lock-constant-face ) + ( "co" . font-lock-comment-face ) + ( "st" . font-lock-string-face ) + ( "bi" . font-lock-builtin-face ) + + ( "cb" . matlab-cellbreak-face ) + ( "ig" . matlab-ignored-comment-face ) + ( "pr" . matlab-pragma-face ) + ( "cd" . matlab-commanddual-string-face ) + ( "us" . matlab-unterminated-string-face ) + ( "ma" . matlab-math-face ) + ( "si" . matlab-simulink-keyword-face ) + + ( "bo" . bold ) + ( "df" . nil ) + ) + "List of testing keywords and associated faces.") + + +(defvar met-complete-files '("complete.m") + "List of files for running font completion tests.") + +(defvar met-complete-tools '((var . matlab-find-recent-variable) + (fcn . matlab-find-user-functions) + ) + "List of tools that generate completions.") + +(defvar metest-complete-test (cons "completion" met-complete-files)) +(defun metest-complete-test (F) + "Test the completion tools in matlab-complete.el" + (let ((buf (metest-find-file F)) + exp act + (cnt 0)) + (with-current-buffer buf + (goto-char (point-min)) + + (while (re-search-forward "%\\s-*@@" nil t) + (setq exp (read (buffer-substring-no-properties (point) (scan-sexps (point) 1)))) + ;; Move to end of previous line, and try to do a complete + (matlab-with-context-line (matlab-previous-code-line (matlab-compute-line-context 2)) + (end-of-line) + (let* ((prefix (buffer-substring-no-properties + (save-excursion (forward-word -1) (point)) + (point))) + (sem (matlab-lattr-semantics prefix)) + ) + ;; Did we get the expected semantics of this location? + (when (not (eq sem (car exp))) + (metest-error "Completion Semantic Missmatch: Expected %s but found %s" (car exp) sem)) + + (let* ((expR (nthcdr 2 exp)) + (fcn (assoc (nth 1 exp) met-complete-tools)) + (act (funcall (cdr fcn) prefix))) + (when (not (equal act expR)) + (metest-error "Completion Missmatch: Expected %S but found %S using function %S" + expR act fcn)) + ) + )) + (setq cnt (1+ cnt)) + ;; Skip this match, find the next. + (end-of-line))) + (list cnt "tests"))) + + +(defvar met-fontlock-files '("fontlock.m" "mclass.m" "blocks.m") + "List of files for running font lock tests.") + +(defvar metest-fontlock-test (cons "font lock" met-fontlock-files)) +(defun metest-fontlock-test (F) + "Run the semantic parsing test to make sure the parse works." + (let ((buf (metest-find-file F)) + (noninteractive nil) ;; fake out font lock + (cnt 0) (fntcnt 0)) + (with-current-buffer buf + + (goto-char (point-min)) + + (let ((md (match-data))) + ;; Force font lock to throw catchable errors. + (font-lock-mode 1) + (font-lock-flush (point-min) (point-max)) + (font-lock-ensure (point-min) (point-max)) + (font-lock-fontify-region (point-min) (point-max)) + + ;; FL test 1: make sure font lock is on and match data didn't change. + (unless font-lock-mode + (metest-error "Font Lock failed to turn on.")) + ;;(unless (equal md (match-data)) + ;; (metest-error "Font Locking transmuted the match data")) + (when (not (get-text-property 2 'fontified)) + (metest-error "Font Lock Failure: can't run test because font lock failed to fontify region.")) + ) + + ;; Lines that start with %^ comments are FL keyword test features. + ;; Find the line, then look for every ^ and find it's column and match + ;; to previous line's column. + (while (re-search-forward "^\\s-*%\\(?: \\$\\$\\$\\)?\\^" nil t) + (let ((next (point-at-eol)) + (prevstart (save-excursion (forward-line -1) (point-at-bol))) + ) + (while (re-search-forward "\\^\\(\\w\\w\\)\\>" (point-at-eol) t) + (let* ((col (- (match-beginning 0) (point-at-bol))) + (fk (match-string-no-properties 1)) + (pt (+ prevstart col)) + (fnt (get-text-property pt 'face)) + (fnt1 (if (consp fnt) (car fnt) fnt)) + (fnt2 (if (consp fnt) (nth 1 fnt) nil)) + (exp (cdr (assoc fk met-kw-font-alist)))) + + (cond + ((consp exp) + (when (not (eq (car exp) fnt1)) + (metest-error "Bad font layer 1 found @ col %d: Expected %S but found %S" + col (car exp) fnt1)) + (when (not (eq (nth 1 exp) fnt2)) + (metest-error "Bad font layer 2 found @ col %d: Expected %S but found %S" + col (nth 1 exp) fnt2))) + (t + (when (not (eq exp fnt1)) + (metest-error "Bad font found @ col %d: Expected %S but found %S" + col exp fnt)))) + + (setq fntcnt (1+ fntcnt)) + )) + (goto-char next) + (setq cnt (1+ cnt)))) + + (list cnt "lines with " fntcnt "fonts tested")))) + +;;; UTILS +;; + +(defun metest-find-file (file) + "Read FILE into a buffer and return it. +Do error checking to provide easier debugging." + (let ((F (expand-file-name file met-testfile-path))) + (unless (file-exists-p F) + (error "Test file %s does not exist in %s" file met-testfile-path)) + (find-file-noselect F))) + +(defvar metest-error-context-lines 4) +(defun metest-error (&rest args) + "Produce an err with standardized file/line prefix." + (declare (indent 1)) + (let* ((lineno (line-number-at-pos)) + (fname (file-name-nondirectory (buffer-file-name))) + (pre (format "\n%s:%d: Error: " fname lineno)) + (post (apply 'format args)) + (prelines (min lineno metest-error-context-lines))) + (message "\n--vv buffer snip: %s vv--" fname) + (save-excursion + (forward-line (- prelines)) + (while (> prelines 0) + (message "|%s" (buffer-substring-no-properties (point-at-bol) (point-at-eol))) + (forward-line 1) + (setq prelines (1- prelines))) + (message ">%s" (buffer-substring-no-properties (point-at-bol) (point-at-eol))) + (forward-line 1) + (while (and (> metest-error-context-lines prelines) (not (eobp))) + (message "|%s" (buffer-substring-no-properties (point-at-bol) (point-at-eol))) + (forward-line 1) + (setq prelines (1+ prelines)))) + (message "---^^ buffer snip ^^---") + (setq metest-test-error t) + (error (concat pre post)))) + +;;; Logging prormance data for the tests +;; +(defvar metest-log-file "metest_timing_log.dat" + "File to store timing data to.") + +(defvar metest-time-log nil + "Data stored for each run.") + +(defun metest-log-init () + "Init the log file and data variable." + (setq metest-time-log nil) + ) + +(defun metest-shorten (sym) + "Convert SYM into a column header." + (let ((str (symbol-name sym))) + (substring str 7 -5))) + +(defun metest-log-write () + "Write dta into our log file." + (save-current-buffer + (set-buffer (find-file-noselect metest-log-file)) + (let ((LOG (reverse metest-time-log))) + (when (= (point-min) (point-max)) + ;; Initialize the new buffer + (insert "Time\t") + (insert (mapconcat (lambda (log) (metest-shorten (car log))) LOG "\t"))) + ;; Insert our measurements + (goto-char (point-max)) + (newline) + (insert (format-time-string "\"%Y/%m/%d %H:%M\"\t" (current-time))) + (insert (mapconcat (lambda (log2) (format "%f" (cdr log2))) LOG "\t")) + (save-buffer) + ;; Go back and find our baseline and return it. + (goto-char (point-min)) + (forward-line 1) + (read (concat "(" (buffer-substring-no-properties (point-at-bol) (point-at-eol)) ")")) + ))) + +(defun metest-log-report (baseline) + "Report via message what happened during the test suite." + (let ((log (reverse metest-time-log)) + (base (cdr baseline))) + (princ "Baseln\tRun\tImprovement\tTest\n") + (while (and log base) + (princ (format "%.4f\t" (car base))) + (princ (format "%.4f\t" (cdr (car log)))) + (princ (format "%.4f\t\t" (- (car base) (cdr (car log))))) + (princ (metest-shorten (car (car log)))) + (princ "\n") + (setq log (cdr log) + base (cdr base))) + )) + +(defun metest-timeit (fcn &optional file) + "Time running FCN and save result in LOGFILE. +Use this to track perforamnce improvements during development automatically." + (let* ((start (current-time)) + (out (funcall fcn file)) + (end (current-time)) + (diff (float-time (time-subtract end start)))) + (if (eq fcn (car-safe (car-safe metest-time-log))) + ;; Same fcn, append our number + (setcdr (car metest-time-log) (+ diff (cdr (car metest-time-log)))) + (push (cons fcn diff) metest-time-log)) + (cons diff out))) + (provide 'metest) ;;; metest.el ends here diff --git a/tests/mfuncends.m b/tests/mfuncends.m new file mode 100644 index 0000000..9201469 --- /dev/null +++ b/tests/mfuncends.m @@ -0,0 +1,26 @@ +function mfuncends( ) +% Test function that has ends for each function. !!0 +% !!0 +% Used with test harness to validate indentation and end detection. !!0 +% !!0 +% %%%function function function + + fcn_call(); %!!4 + + if condition %!!4 + fcn_call(); %!!8 + fcn_call ... !!8 + (); %!!12 + end %!!4 + +end%!!0 - also no space after end + +function a=fcn_call(inp) %!!0 + + while inp > 0 %!!4 + fcn_call(inp-1); %!!8 + a = [ 1 2 ... !!8 + 3 4 ]; %!!14 + end %!!4 + +end %!!0 diff --git a/tests/mfuncnoend.m b/tests/mfuncnoend.m new file mode 100644 index 0000000..51e12ab --- /dev/null +++ b/tests/mfuncnoend.m @@ -0,0 +1,15 @@ +function mfuncnoend +% A function file that does not have ends at the end of functions. +% +% %%% function nil nil + +fcn_call(1) + + +function fcn_call(idx) + +if idx > 0 + fcn_call(ix-1) +end + + diff --git a/tests/mfuncnoendblock.m b/tests/mfuncnoendblock.m new file mode 100644 index 0000000..18a2fa6 --- /dev/null +++ b/tests/mfuncnoendblock.m @@ -0,0 +1,33 @@ +%{ + Add a block comment at the beginning to skip over. + x % !!2 +% !! 0 +%} +function mfuncnoendblock +% A function file that does not have ends at the end of functions. +% !!0 +% %%% function nil nil + +fcn_call(1) %!!0 + + +function fcn_call(idx) %!!0 + +if idx > 0 %!!0 + fcn_call(ix-1) %!!4 + goo(3); +end %!!0 + +function c=goo(p3) %!!0 +if p3 < 3 %!!0 + if p3 < 0 %!!4 + c = p3 * -3; %!!8 + else %!!4 + c = p3 * 3; %!!8 + for i=1:10 %!!8 + c = c + i; %!!12 + end %!!8 + end %!!4 +else %!!0 + c=p3*4; %!!4 +end %!!0 diff --git a/tests/mfuncnoendindent.m b/tests/mfuncnoendindent.m new file mode 100644 index 0000000..18f6cc2 --- /dev/null +++ b/tests/mfuncnoendindent.m @@ -0,0 +1,15 @@ +function mfuncnoendindent +% A function file that does not have ends at the end of functions. +% +% %%% function nil t + + fcn_call(1) + + +function fcn_call(idx) + + if idx > 0 + fcn_call(ix-1) + end + + diff --git a/tests/mfuncnofuncindent.m b/tests/mfuncnofuncindent.m new file mode 100644 index 0000000..e6f5be2 --- /dev/null +++ b/tests/mfuncnofuncindent.m @@ -0,0 +1,32 @@ +function mfuncnofuncindent( ) +% Test function that has ends for each function. !!0 +% !!0 +% Used with test harness to validate indentation and end detection. !!0 +% !!0 +% %%%function function nil + +fcn_call(); %!!0 + +if condition %!!0 + fcn_call(); %!!4 + fcn_call ... !!4 + (); %!!8 +end %!!0 + +end%!!0 - also no space after end + +function a=fcn_call(inp) %!!0 + +while inp > 0 %!!0 + fcn_call(inp-1); %!!4 + a = [ 1 2 ... !!4 + 3 4 ]; %!!10 +end %!!0 + +end %!!0 + +%{ +% Local Variables: +% matlab-indent-function-body: nil +% End: +%} \ No newline at end of file diff --git a/tests/mfuncspacey.m b/tests/mfuncspacey.m new file mode 100644 index 0000000..53c708b --- /dev/null +++ b/tests/mfuncspacey.m @@ -0,0 +1,11 @@ + function mfuncspacey( ) + % Test function that has ends for each function. + % + % Used with test harness to validate indentation and end detection. + % + % %%%function function function + + fcn_call(); + + end + diff --git a/tests/stringtest.m b/tests/stringtest.m index c239dcd..c47437b 100644 --- a/tests/stringtest.m +++ b/tests/stringtest.m @@ -1,6 +1,8 @@ %% Tests for char vector and string handling. % % #c# +% +% %%%script script script %% Basic strings @@ -13,8 +15,8 @@ stringscalar = "string scalar #s#"; % Comment with 'character vector #c#' in it. % Comment with "string scalar #c#" in it. -charvi = 'char vector incomplete #v# -stringi = "string scalar incomplete #s# +charvi = 'char vector incomplete #V# +stringi = "string scalar incomplete #S# % Comment with 'char vector incomplete #c# % Comment with "string scalar incomplete #c# @@ -27,16 +29,16 @@ stringcv = "string scalar with 'char vec #s#' in it"; chard = 'char vector with '' in it #v#'; stringd = "string scalar with "" in it #s#"; -chardi = 'incomplete char vector with '' in it #v# -stringdi = "incomplete string scalar with "" in it #s# +chardi = 'incomplete char vector with '' in it #V# +stringdi = "incomplete string scalar with "" in it #S# %% Strings with Comments charvc = 'char vector with % comment char #v#'; stringc = "string scalar with % comment char #s#"; -charvci = 'incomplete char vector with % comment char #v# -stringci = "incomplete string scalar with % comment char #s# +charvci = 'incomplete char vector with % comment char #V# +stringci = "incomplete string scalar with % comment char #S# charvbc = 'char vector with %{ comment char #v# %} '; stringbc = "string scalar with %{ comment char #s# %} "; @@ -69,19 +71,24 @@ icell_in_strs2_nested = { 'charv innercell " #v# }' "strinc innercel ' #s# }" %% Elipsis as comment fun_call(); ... This is a comment after an elipsis #e# -fun_call(); ... 'charvec in elipsis comment #e#' -fun_call(); ... "string in elipsis comment #e#" -fun_call(); ... % comment after an elipsis is still elipsis #e# +fun_call(); ... 'charvec in elipsis comment #e#' + +fun_call(); ... "string in elipsis comment #e#" + +fun_call(); ... % comment after an elipsis is still elipsis #e# + %% Elipsis and strings and other comments Ecv = 'string with ... in #v# it'; Es = "string with ... in #s# it"; % Comment with ... in it #c# +eecv = '...'; % string with only ellipsis in it #c# +ees = "..."; % string with only ellipsis in it #c# -x = [ 'foo bar',newline,... - ' ''-goo'', ... #v#',newline,... - ' ''-bar'', ... #v#',newline]; +x = [ 'foo bar', newline, ... #e# + ' ''-goo'', ... #v#', newline, ... #e# + ' ''-bar'', ... #v#', newline ]; func_call1('function with charvec', ... #e# 'after ellipsis charvec with ellipsis ... #v#'); @@ -141,7 +148,7 @@ else Cs = "not unreachable #s#"; end -%% Block Comments #c# +%% Block Comments #C# %{ @@ -154,4 +161,34 @@ end not_commented(); +%{ just a regular comment #c# %} should_be_comment #c# + % Normal comment #c# + +%% Ignored Comments #C# + +% $$$ This comment is ignored by indentation engine. #i# + +%^ This comment is igored too, used in tests for font lock. #i# + + +%% Command line dual #C# +% Note: stuff after a symbol treated as string + +disp _this is string input to function #d#_ +disp _this is also string input to a function #d#_ + +regularcode; #r# + +% Note: Case sensitivity of cmd dual functions +DISP _regular code even though ML would treat as cmd dual #r#_ + +%{ +% Local Variables: +% matlab-syntax-support-command-dual: t +% matlab-show-mlint-warnings: nil +% End: +%} + + +%% END diff --git a/toolbox/emacsrun.m b/toolbox/emacsrun.m new file mode 100644 index 0000000..79e7ae2 --- /dev/null +++ b/toolbox/emacsrun.m @@ -0,0 +1,28 @@ +function emacsrun(mfile, varargin) +% Run code from MFILE. +% Assumes MFILE was recently edited, and proactively clears that function. +% +% Command sent by Emacs for save-and-go functionality + + % Now figure out if shortFileName is on the path. + [ fullFilePath, shortFileName ] = fileparts(mfile); + onpath = ~isempty(which(shortFileName)); + + if ~exist(fullFilePath,'file') + error('You must save your file into a location accessible by MATLAB process.'); + end + + + % If not on the path, temporarilly switch to that directory so it and an files it references are + % accessible + if ~onpath + oldpath = pwd; + cd(fullFilePath); + cleanup = onCleanup(@()cd(oldpath)); + end + clear(shortFileName); + + cmd = [ shortFileName varargin{:} ]; + + evalin('base',cmd); +end \ No newline at end of file