342 lines
10 KiB
EmacsLisp
342 lines
10 KiB
EmacsLisp
|
;;; matlab-scan.el --- Tools for contextually scanning a MATLAB buffer
|
||
|
;;
|
||
|
;; Copyright (C) 2021 Eric Ludlam
|
||
|
;;
|
||
|
;; Author: <eludlam@mathworks.com>
|
||
|
;;
|
||
|
;; 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
|
||
|
;;
|
||
|
(defconst matlab-block-keyword-list '(("end" . end)
|
||
|
("function" . decl)
|
||
|
("classdef" . decl)
|
||
|
("arguments" . args)
|
||
|
("properties" . mcos)
|
||
|
("methods" . mcos)
|
||
|
("events" . mcos)
|
||
|
("enumeration" . mcos)
|
||
|
("if" . ctrl)
|
||
|
("else" . mid)
|
||
|
("ifelse" . mid)
|
||
|
("for" . ctrl)
|
||
|
("parfor" . ctrl)
|
||
|
("while" . ctrl)
|
||
|
("switch" . ctrl)
|
||
|
("case" . case)
|
||
|
("otherwise" . case)
|
||
|
("try" . ctrl)
|
||
|
("catch" . mid)
|
||
|
)
|
||
|
"List of keywords that are part of code blocks.")
|
||
|
|
||
|
(defconst matlab-keyword-table
|
||
|
(let ((ans (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.")
|
||
|
|
||
|
;;; 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)
|
||
|
"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.
|
||
|
This function caches compuated context onto the line it is for, and will
|
||
|
return the cache if it finds it."
|
||
|
(cond ((= level 1)
|
||
|
(let ((ctxt (matlab-scan-cache-get 1)))
|
||
|
(unless ctxt
|
||
|
(setq ctxt (matlab-compute-line-context-lvl-1))
|
||
|
(matlab-scan-cache-put ctxt 1))
|
||
|
ctxt))
|
||
|
(t
|
||
|
nil))
|
||
|
)
|
||
|
|
||
|
(defconst mlf-ltype 0)
|
||
|
(defconst mlf-stype 1)
|
||
|
(defconst mlf-indent 2)
|
||
|
(defconst mlf-paren-depth 3)
|
||
|
(defconst mlf-paren-char 4)
|
||
|
(defconst mlf-paren-col 5)
|
||
|
(defconst mlf-paren-delta 6)
|
||
|
(defconst mlf-end-comment-type 7)
|
||
|
(defconst mlf-end-comment-col 8)
|
||
|
|
||
|
(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.
|
||
|
Return list has these fields:
|
||
|
0 - Primary line type
|
||
|
1 - Secondary line type
|
||
|
2 - Indentation
|
||
|
3 - Parenthisis depth
|
||
|
4 - Char for innermost beginning paren
|
||
|
5 - Column of innermost beginning paren
|
||
|
6 - Parenthisis depth change on this line
|
||
|
7 - End Comment type (ellipsis, comment, or nil)
|
||
|
8 - End Comment start column
|
||
|
"
|
||
|
(save-excursion
|
||
|
(back-to-indentation)
|
||
|
(let* ((ppsend (save-excursion (syntax-ppss (point-at-eol))))
|
||
|
(pps (syntax-ppss (point)))
|
||
|
(ltype 'empty)
|
||
|
(stype nil)
|
||
|
(cc 0)
|
||
|
(paren-depth (nth 0 pps))
|
||
|
(paren-char nil)
|
||
|
(paren-col nil)
|
||
|
(paren-delta (- (car pps) (car ppsend)))
|
||
|
(ec-type nil)
|
||
|
(ec-col 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-char (char-after (point))
|
||
|
paren-col (current-column))))
|
||
|
|
||
|
(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-comment-end)
|
||
|
((looking-at "%")
|
||
|
'block-comment-body-prefix)
|
||
|
(t 'block-comment-body))))
|
||
|
|
||
|
;; 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-comment-start)
|
||
|
((looking-at "%%")
|
||
|
'cell-start)
|
||
|
((looking-at "% \\$\\$\\$")
|
||
|
'indent-ignore)
|
||
|
(t nil))))
|
||
|
|
||
|
;; Looking at word constituent. If so, identify if it is one of our
|
||
|
;; special identifiers.
|
||
|
((looking-at "\\w+\\>")
|
||
|
(let* ((word (match-string-no-properties 0))
|
||
|
(sym (intern-soft word matlab-keyword-table))
|
||
|
)
|
||
|
(if sym
|
||
|
(if (eq (symbol-value sym) 'end)
|
||
|
;; Special end keyword is in a class all it's own
|
||
|
(setq ltype 'end)
|
||
|
;; If we found this in our keyword table, then it is a start
|
||
|
;; of a block with a subtype.
|
||
|
(setq ltype 'block-start
|
||
|
stype (symbol-value sym)))
|
||
|
;; Else - not a sym - just some random code.
|
||
|
(setq ltype 'code)
|
||
|
)))
|
||
|
|
||
|
;; 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 csc
|
||
|
(setq ec-col csc
|
||
|
ec-type (if (= (char-after csc) ?\%) 'comment 'ellipsis)) ;; type
|
||
|
))
|
||
|
|
||
|
(list ltype stype cc paren-depth paren-char paren-col paren-delta ec-type ec-col)
|
||
|
)))
|
||
|
|
||
|
|
||
|
(defun matlab-compute-line-context-lvl-2 (lvl1)
|
||
|
"Compute level 2 line contexts for indentation.
|
||
|
These are more expensive checks queued off of a lvl1 context."
|
||
|
|
||
|
;; matlab-ltype-help-comm
|
||
|
|
||
|
;; block start
|
||
|
;; block end
|
||
|
;; change in # blocks on this line.
|
||
|
|
||
|
)
|
||
|
|
||
|
;;; Accessor Utilities
|
||
|
;;
|
||
|
;; Use these to query a context for a piece of data
|
||
|
(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 based on LVL1 cache."
|
||
|
(eq (car lvl1) 'comment))
|
||
|
|
||
|
(defsubst matlab-line-comment-ignore-p (lvl1)
|
||
|
"Return t if the current line is a comment based on LVL1 cache."
|
||
|
(and (matlab-line-comment-p lvl1) (eq (nth mlf-stype lvl1) 'indent-ignore)))
|
||
|
|
||
|
(defsubst matlab-line-block-start-keyword-p (lvl1)
|
||
|
"Return t if the current line starts with block keyword."
|
||
|
(eq (car lvl1) 'block-start))
|
||
|
|
||
|
;; Code and Declarations
|
||
|
(defsubst matlab-line-declaration-p (lvl1)
|
||
|
"If the current line is a declaration, return the column it starts on.
|
||
|
Declarations are things like function or classdef."
|
||
|
(and (matlab-line-block-start-keyword-p lvl1) (eq (nth mlf-stype lvl1) 'decl)))
|
||
|
|
||
|
;;; 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-line-comment-help-p (lvl1)
|
||
|
"Return declaration column if the current line is part of a help comment.
|
||
|
Declarations are things like functions and classdefs.
|
||
|
Indentation a help comment depends on the column of the declaration."
|
||
|
(and (matlab-line-comment-p lvl1)
|
||
|
(save-excursion
|
||
|
(beginning-of-line)
|
||
|
(forward-comment -100000)
|
||
|
(let ((c-lvl1 (matlab-compute-line-context 1)))
|
||
|
(when (matlab-line-declaration-p c-lvl1)
|
||
|
(current-indentation)))
|
||
|
)))
|
||
|
|
||
|
|
||
|
;;; 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)
|
||
|
|
||
|
(defun matlab-scan-cache-get (level)
|
||
|
"Get a cached context at level."
|
||
|
;; TODO
|
||
|
nil)
|
||
|
|
||
|
(defun matlab-scan-cache-put (ctxt level)
|
||
|
"Get a cached context at level."
|
||
|
;; TODO
|
||
|
nil)
|
||
|
|
||
|
|
||
|
(defun matlab-scan-after-change-fcn (start end length)
|
||
|
"Function run in after change hooks."
|
||
|
(setq matlab-scan-temporal-cache nil))
|
||
|
|
||
|
(defun matlab-scan-setup ()
|
||
|
"Setup use of the indent cache for the current buffer."
|
||
|
(interactive)
|
||
|
(add-hook 'after-change-functions 'matlab-scan-after-change-fcn t)
|
||
|
)
|
||
|
|
||
|
(defun matlab-scan-disable ()
|
||
|
"Setup use of the indent cache for the current buffer."
|
||
|
(interactive)
|
||
|
(remove-hook 'after-change-functions 'matlab-scan-after-change-fcn t)
|
||
|
)
|
||
|
|
||
|
;;; Debugging and Querying
|
||
|
;;
|
||
|
(defun matlab-describe-line-indent-context ()
|
||
|
"Describe the indentation context for the current line."
|
||
|
(interactive)
|
||
|
(let* ((lvl1 (matlab-compute-line-context 1))
|
||
|
(paren-char (nth mlf-paren-char lvl1))
|
||
|
(open (format "%c" (or paren-char ?\()))
|
||
|
(close (format "%c"
|
||
|
(cond ((not paren-char) ?\))
|
||
|
((= paren-char ?\() ?\))
|
||
|
((= paren-char ?\[) ?\])
|
||
|
((= paren-char ?\{) ?\})
|
||
|
(t ??))))
|
||
|
(extraopen "")
|
||
|
(extraclose "")
|
||
|
)
|
||
|
(when (= (nth mlf-paren-depth lvl1) 0)
|
||
|
(setq open (propertize open 'face 'shadow)
|
||
|
close (propertize close 'face 'shadow)))
|
||
|
(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)))))
|
||
|
|
||
|
|
||
|
(message "%s%s %s%s%d%s%s %s" (nth mlf-ltype lvl1)
|
||
|
(format " %s" (or (nth mlf-stype lvl1) ""))
|
||
|
;; paren system
|
||
|
extraopen
|
||
|
open
|
||
|
(nth mlf-paren-depth lvl1)
|
||
|
close
|
||
|
extraclose
|
||
|
(cond ((eq (nth mlf-end-comment-type lvl1) 'comment) "%")
|
||
|
((eq (nth mlf-end-comment-type lvl1) 'ellipsis) "...")
|
||
|
(t ""))
|
||
|
)
|
||
|
|
||
|
)
|
||
|
)
|
||
|
|
||
|
|
||
|
(provide 'matlab-scan)
|
||
|
|
||
|
;;; matlab-indent.el ends here
|