├── README.md ├── company-matlab.el ├── doc-matlab.el ├── flycheck-matlab.el ├── image ├── file.png ├── flycheck-demo.png └── shell.png ├── jtd-matlab.el ├── matlab-mode.el ├── matlab-server.el └── matlab.el /README.md: -------------------------------------------------------------------------------- 1 | # matlab-mode 2 | 3 | An emacs matlab mode based on the [classic one](http://matlab-emacs.sourceforge.net/). 4 | 5 | ## Warning 6 | 1. First use or open ```.m``` file can be slow because it needs to open a background matlab process. 7 | 2. You need to set ```matlab-server-executable``` to where your matlab exec. 8 | 9 | ## Highlights 10 | 11 | 1. A company-mode backend 12 | ![company-file](./image/file.png) 13 | ![company-shell](./image/shell.png) 14 | 15 | 2. A flycheck backend 16 | ![demo](./image/flycheck-demo.png) 17 | 18 | 3. Easy to view document of word at cursor (default keybinding: Ctrl-c + h). 19 | 20 | 4. Jump to definition (default keybinding: Ctrl-c + s). 21 | Currently, this only helps open the souce file (can not go to the definition of the function). 22 | If it does not work, it may be becasue you have not added the file's folder into matlab workspace. 23 | 24 | ## Install 25 | 26 | 1. Install dependencies like ```s.el```, ```flycheck``` and ```company-mode```. 27 | 2. Copy the code to somewhere 28 | 3. Add the following to the initialization file 29 | 30 | ```elisp 31 | (setq matlab-server-executable "/path/to/matlab/binary") 32 | (add-to-list 'load-path "/path/to/matlab-mode/") 33 | (require 'matlab-mode) 34 | (matlab-mode-common-setup) 35 | ``` 36 | 37 | ## Usage 38 | To use the functionalities, one must first run M-x ```matlab-start``` to start the background process. 39 | Since matlab is a bit slow to start, you may need to wait a few seconds. 40 | 41 | ## Recommendation 42 | Recommend to turn-off automatic complete in company-mode and do the completion when needed. 43 | 44 | ## Copyright 45 | 46 | ```matlab.el``` is mostly copied from the ["classic" matlab mode](http://matlab-emacs.sourceforge.net/). 47 | 48 | ## TODO 49 | 50 | 1. debug? 51 | -------------------------------------------------------------------------------- /company-matlab.el: -------------------------------------------------------------------------------- 1 | ;;; company-matlab.el --- support for the Matlab programming language 2 | 3 | ;; Copyright (C) 2016 yuhonglin 4 | 5 | ;; Author: yuhonglin 6 | ;; Keywords: languages, terminals 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; The company backend of matlab mode. 24 | ;; It is based on matlab-server.el. 25 | 26 | ;;; Code: 27 | 28 | (require 'cl-lib) 29 | (require 'company) 30 | (require 'matlab) 31 | (require 's) 32 | (require 'matlab-server) 33 | 34 | (defvar company-matlab-is-completing nil 35 | "Whether it is during the completing backend.") 36 | 37 | (defvar company-matlab-complete-output "" 38 | "The output of matlab used by completing") 39 | 40 | (defvar company-matlab-complete-candidates "" 41 | "The candidates") 42 | 43 | (defun company-matlab-process-received-data (inarg) 44 | (mapcar 'car 45 | (mapcar 'cdr 46 | (s-match-strings-all "'\\([^ \t\n']+\\)'" inarg)))) 47 | 48 | 49 | ;; some are copied from matlab-shell-collect-command-output function 50 | (defun company-matlab-get-candidates (arg) 51 | (if (and matlab-server-process 52 | (eq (process-status matlab-server-process) 'run)) 53 | (let ((res (company-matlab-process-received-data 54 | (matlab-send-request-sync 55 | (concat "tmp=com.mathworks.jmi.MatlabMCR;tmp.mtFindAllTabCompletions('" 56 | arg "'," (int-to-string (length arg)) ",0)"))))) 57 | (if (eq res nil) 58 | ;; nil 59 | (company-other-backend) 60 | res)))) 61 | 62 | 63 | (defun company-matlab-grab-symbol () 64 | (let* ((prefix (buffer-substring (point) 65 | (save-excursion (skip-chars-backward "a-zA-Z0-9._") 66 | (point))))) 67 | (if (or (= (length prefix) 0) 68 | (string= (substring prefix 0 1) ".") 69 | (string= (substring prefix 0 1) "/")) 70 | nil 71 | prefix))) 72 | 73 | 74 | (defun company-matlab (command &optional arg &rest ignored) 75 | (interactive (list 'interactive)) 76 | (cl-case command 77 | (interactive (company-begin-backend 'company-matlab)) 78 | (prefix (and (or (eq major-mode 'matlab-mode) 79 | (string= (buffer-name) "*MATLAB*")) 80 | (company-matlab-grab-symbol))) 81 | (no-cache nil) 82 | (duplicates t) 83 | (ignore-case nil) 84 | (candidates 85 | (company-matlab-get-candidates arg)))) 86 | 87 | 88 | (defun company-matlab-company-cancel-hook (arg)) 89 | 90 | (add-to-list 'company-completion-cancelled-hook 'company-matlab-company-cancel-hook) 91 | 92 | (provide 'company-matlab) 93 | -------------------------------------------------------------------------------- /doc-matlab.el: -------------------------------------------------------------------------------- 1 | (require 'matlab-server) 2 | (require 's) 3 | 4 | (defun doc-matlab-process-received-data (s) 5 | (when (> (length s) 4) 6 | (substring (s-chomp (s-trim s)) 0 -2))) 7 | 8 | (defun doc-matlab-grab-current-word () 9 | (save-excursion 10 | (let (start end oldpos) 11 | (setq oldpos (point)) 12 | (skip-chars-backward "A-Za-z0-9_") (setq start (point)) 13 | (skip-chars-forward "A-Za-z0-9_") (setq end (point)) 14 | (buffer-substring start end)))) 15 | 16 | (defun matlab-view-current-word-doc-in-another-buffer () 17 | "look up the matlab help info and show in another buffer" 18 | (interactive) 19 | (let* ((word (doc-matlab-grab-current-word)) 20 | (doc (doc-matlab-process-received-data 21 | (matlab-send-request-sync 22 | (concat "help " word))))) 23 | (if (= (length doc) 0) 24 | (error (concat "doc of '" word "' not found"))) 25 | 26 | (let ((buffer (get-buffer-create (concat "*matdoc:" word "*")))) 27 | (set-buffer buffer) 28 | (erase-buffer) 29 | (insert doc) 30 | (goto-char (point-min)) 31 | (pop-to-buffer buffer)))) 32 | 33 | 34 | (provide 'doc-matlab) 35 | -------------------------------------------------------------------------------- /flycheck-matlab.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t; -*- 2 | 3 | (require 'flycheck) 4 | (require 'matlab-server) 5 | (require 's) 6 | 7 | (defun flycheck-matlab--verify (checker) 8 | "Verify the matlab syntax checker." 9 | (list 10 | (flycheck-verification-result-new 11 | :label "mlint path checking" 12 | :message (if (and matlab-server-process 13 | (eq (process-status matlab-server-process) 'run)) 14 | "matlab is on" "matlab process not found") 15 | :face (if t 16 | 'success '(bold error)) ))) 17 | 18 | (defun flycheck-matlab-get-error (filepath) 19 | (if (and matlab-server-process 20 | (eq (process-status matlab-server-process) 'run)) 21 | (let ((rawerrstr (matlab-send-request-sync 22 | (s-trim (concat "arrayfun(@(x) display(sprintf('%d\t%d\t%s', x.line, x.column(1), x.message)), checkcode('" 23 | filepath "'))"))))) 24 | (delq nil 25 | (mapcar (lambda (rrs) 26 | (let* ((cleanrs (s-replace ">>" "" rrs)) 27 | (cleanrs-split (s-split "\t" (s-trim cleanrs)))) 28 | (when (= (length cleanrs-split) 3) 29 | (cl-multiple-value-bind (lpostart cpostart estr) cleanrs-split 30 | `(,(string-to-int lpostart) ,(string-to-int lpostart) ,(string-to-int cpostart) ,(string-to-int cpostart) ,estr))))) 31 | (s-split "\n" rawerrstr)))))) 32 | 33 | 34 | (defun flycheck-matlab--start (checker callback) 35 | (let* ((rawerror (flycheck-matlab-get-error (buffer-file-name))) 36 | (errors (mapcar (lambda (rerr) 37 | (cl-multiple-value-bind (lpostart lposend cpostart cposend estr) rerr 38 | (flycheck-error-new-at lpostart cpostart 'warning estr :checker checker))) 39 | rawerror))) 40 | (funcall callback 'finished errors))) 41 | 42 | 43 | (flycheck-define-generic-checker 'flycheck-matlab 44 | "A syntax checker for matlab." 45 | :start #'flycheck-matlab--start 46 | :verify #'flycheck-matlab--verify 47 | :modes '(matlab-mode) 48 | ;; :error-filter flycheck-matlab-error-filter 49 | :predicate (lambda () 50 | (eq major-mode 'matlab-mode))) 51 | 52 | (add-to-list 'flycheck-checkers 'flycheck-matlab) 53 | 54 | (provide 'flycheck-matlab) 55 | -------------------------------------------------------------------------------- /image/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuhonglin/matlab-mode/3ec032e536d0d18efad84a010099e3a6e462f5af/image/file.png -------------------------------------------------------------------------------- /image/flycheck-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuhonglin/matlab-mode/3ec032e536d0d18efad84a010099e3a6e462f5af/image/flycheck-demo.png -------------------------------------------------------------------------------- /image/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuhonglin/matlab-mode/3ec032e536d0d18efad84a010099e3a6e462f5af/image/shell.png -------------------------------------------------------------------------------- /jtd-matlab.el: -------------------------------------------------------------------------------- 1 | ;; jump-to-definition of matlab 2 | 3 | (require 's) 4 | (require 'matlab-server) 5 | 6 | 7 | ;; the problem is that, sometime the return of `which` 8 | ;; function in matlab contains file extensions '.m', 9 | ;; but sometime does not. 10 | (defun jtd-matlab-process-received-data (s) 11 | (when (> (length s) 4) 12 | (let ((rawreturn (car (s-match "/[^><()\t\n,'\";:]+\\(:[0-9]+\\)?" 13 | (s-chomp (s-trim s)))))) 14 | (if (= 2 (length (s-shared-end ".m" rawreturn))) 15 | rawreturn ;; contains '.m' 16 | (if (file-exists-p (concat rawreturn ".m")) 17 | (concat rawreturn ".m") ;; append the '.m' 18 | ;; else, may be a folder 19 | rawreturn))))) 20 | 21 | (defun jtd-matlab-grab-current-word () 22 | (save-excursion 23 | (let (start end oldpos) 24 | (setq oldpos (point)) 25 | (skip-chars-backward "A-Za-z0-9_") (setq start (point)) 26 | (skip-chars-forward "A-Za-z0-9_") (setq end (point)) 27 | (buffer-substring start end)))) 28 | 29 | (defun matlab-jump-to-definition-of-word-at-cursor () 30 | "open the source code of the word at cursor" 31 | (interactive) 32 | (if (not (buffer-file-name)) 33 | (error "jtd-matlab: you should save the file first")) 34 | 35 | (let* ((word (jtd-matlab-grab-current-word)) 36 | (scpath (jtd-matlab-process-received-data 37 | (matlab-send-request-sync 38 | (concat "which('" word "', 'in', " "'" (file-truename (buffer-file-name)) "')"))))) 39 | (if scpath 40 | (find-file scpath) 41 | (error "can not find the definition")))) 42 | 43 | 44 | (provide 'jtd-matlab) 45 | -------------------------------------------------------------------------------- /matlab-mode.el: -------------------------------------------------------------------------------- 1 | (require 'matlab) 2 | (require 'matlab-server) 3 | (require 'company-matlab) 4 | (require 'flycheck-matlab) 5 | (require 'doc-matlab) 6 | (require 'jtd-matlab) 7 | 8 | 9 | ;; update working path 10 | (defvar matlab-last-work-directory "~") 11 | (add-hook 'buffer-list-update-hook 12 | (lambda () 13 | (if (and matlab-server-process 14 | (eq (process-status matlab-server-process) 'run)) 15 | (if (and (or (string= "matlab-mode" major-mode) 16 | (string= (buffer-name) "*MATLAB*")) 17 | (not (string= matlab-last-work-directory default-directory))) 18 | (progn (matlab-send-request-sync (concat "cd " default-directory)) 19 | (setq matlab-last-work-directory default-directory)))))) 20 | 21 | ;; setup that can be changed 22 | (defun matlab-mode-common-setup () 23 | ;; for .m files 24 | (add-to-list 'matlab-mode-hook 25 | (lambda () 26 | ;; turn on the flycheck mode 27 | (flycheck-mode) 28 | ;; use flycheck only when saving the file or enabling the mode 29 | (setq-local flycheck-check-syntax-automatically '(save mode-enabled)) 30 | ;; bind the key of checking document 31 | (local-set-key (kbd "C-c h") 32 | 'matlab-view-current-word-doc-in-another-buffer) 33 | ;; bind the key of jump to source code 34 | (local-set-key (kbd "C-c s") 35 | 'matlab-jump-to-definition-of-word-at-cursor) 36 | ;; set company-backends 37 | (setq-local company-backends '(company-files (company-matlab company-dabbrev))) 38 | )) 39 | 40 | ;; for matlab-shell 41 | (add-to-list 'matlab-shell-mode-hook 42 | (lambda () 43 | ;; set company-backends 44 | (setq-local company-backends '((company-dabbrev company-matlab) company-files ))))) 45 | 46 | 47 | (provide 'matlab-mode) 48 | -------------------------------------------------------------------------------- /matlab-server.el: -------------------------------------------------------------------------------- 1 | ;; Only API: matlab-send-request-sync (request &rest args) 2 | ;; return command output as string 3 | 4 | (defvar matlab-server-executable "matlab") 5 | (defvar matlab-server-process nil) 6 | (defvar matlab-server-buffer " *matlab* " 7 | "The name of the buffer for the matlab process to run in") 8 | (defvar matlab-eot ">>") 9 | (defvar matlab-just-startup t) 10 | (defvar matlab-process-auto-start t) 11 | 12 | (defun matlab-start-server-process () 13 | (if (and matlab-server-executable (matlab-check-server-executable)) 14 | (let ((process-connection-type nil) 15 | (process-adaptive-read-buffering nil) 16 | process) 17 | (setq process 18 | (start-process-shell-command 19 | "matlab" 20 | matlab-server-buffer 21 | (format "%s -nodesktop -nosplash" 22 | (shell-quote-argument matlab-server-executable)))) 23 | (buffer-disable-undo matlab-server-buffer) 24 | (set-process-query-on-exit-flag process nil) 25 | (set-process-sentinel process 'matlab-server-process-sentinel) 26 | (set-process-filter process 'matlab-server-process-filter) 27 | (setq matlab-just-startup t) 28 | process) 29 | (error "matlab: can't find the matlab executable, try to set matlab-server-executable"))) 30 | 31 | 32 | (defun matlab-check-server-executable () 33 | (executable-find matlab-server-executable)) 34 | 35 | (defun matlab-get-server-process-create () 36 | (if (and matlab-server-process 37 | (process-live-p matlab-server-process)) 38 | matlab-server-process 39 | (setq matlab-server-process (matlab-start-server-process)))) 40 | 41 | 42 | (defun matlab-server-kill () 43 | "Kill the running matlab process, if any." 44 | (interactive) 45 | (when (and matlab-server-process (process-live-p matlab-server-process)) 46 | (kill-process matlab-server-process) 47 | (setq matlab-server-process nil))) 48 | 49 | 50 | (defun matlab-server-process-sentinel (process event) 51 | (unless (process-live-p process) 52 | (setq matlab-server-process nil) 53 | (message "matlab process stopped"))) 54 | 55 | 56 | (defun matlab-process-server-response (process response) 57 | (let ((sexp response) 58 | (callback (matlab-server-process-pop-callback process))) 59 | (with-demoted-errors "Warning: %S" 60 | (apply (car callback) sexp (cdr callback))))) 61 | 62 | (defun matlab-server-process-filter (process output) 63 | "Handle the output from matlab. 64 | 1. Split by matlab-eot and return the finished responses. 65 | 2. Then call the callbacks from stack on each of the finished responses." 66 | (let ((pbuf (process-buffer process)) 67 | responses) 68 | ;; append output to process buffer 69 | (when (buffer-live-p pbuf) 70 | (with-current-buffer pbuf 71 | (save-excursion 72 | (goto-char (process-mark process)) 73 | (insert output) 74 | (set-marker (process-mark process) (point)) 75 | ;; check if the message is complete based on `matlab-eot' 76 | (goto-char (point-min)) 77 | (while (search-forward matlab-eot nil t) 78 | (let ((response (buffer-substring-no-properties (point-min) 79 | (point)))) 80 | (delete-region (point-min) (point)) 81 | ;; ignore the first process 82 | (setq responses (cons response responses)))) 83 | (goto-char (process-mark process))))) 84 | ;; handle all responses. 85 | (mapc #'(lambda (r) 86 | (matlab-process-server-response process r)) 87 | (nreverse responses)))) 88 | 89 | (defun matlab-server-process-push-callback (p cb) 90 | "push (process callback) into a stack" 91 | (let ((callbacks (process-get p 'matlab-callback-stack))) 92 | (if callbacks 93 | (nconc callbacks (list cb)) 94 | (process-put p 'matlab-callback-stack (list cb))))) 95 | 96 | (defun matlab-server-process-pop-callback (p) 97 | "pop (process callback) into a stack and return the callback popped" 98 | (let ((callbacks (process-get p 'matlab-callback-stack))) 99 | (process-put p 'matlab-callback-stack (cdr callbacks)) 100 | (car callbacks))) 101 | 102 | (defmacro matlab--without-narrowing (&rest body) 103 | "Remove the effect of narrowing for the current buffer. 104 | Note: If `save-excursion' is needed for BODY, it should be used 105 | before calling this macro." 106 | (declare (indent 0) (debug t)) 107 | `(save-restriction 108 | (widen) 109 | (progn ,@body))) 110 | 111 | (defun matlab-send-request (request callback &rest args) 112 | "Send a request to the process. 113 | At the same time push the callback to the stack (number of callbacks 114 | and number of command sent are the same). After this, the filter 115 | will be called when process outputs." 116 | (let ((process (matlab-get-server-process-create)) 117 | (argv (cons request args))) 118 | (when (and process (process-live-p process)) 119 | (matlab-server-process-push-callback process callback) 120 | (matlab--without-narrowing 121 | (process-send-string process 122 | (format "%s\n" (car argv))))))) 123 | 124 | (defvar matlab-sync-id 0 "ID of next sync request.") 125 | (defvar matlab-sync-result '(-1 . nil) 126 | "The car stores the id of the result and cdr stores the return value.") 127 | 128 | (defun matlab-sync-request-callback (response id) 129 | (setq matlab-sync-result (cons id response))) 130 | 131 | (defun matlab-send-request-sync-helper (request &rest args) 132 | "Send a request to matlab and wait for the result. 133 | Called by matlab-send-request-sync" 134 | (let* ((id matlab-sync-id) 135 | (callback (list #'matlab-sync-request-callback id))) 136 | (setq matlab-sync-id (1+ matlab-sync-id)) 137 | (with-local-quit 138 | (let ((process (matlab-get-server-process-create))) 139 | (when process 140 | (apply 'matlab-send-request request callback args) 141 | (while (not (= id (car matlab-sync-result))) 142 | (accept-process-output process)) 143 | (cdr matlab-sync-result)))))) 144 | 145 | (defun matlab-start () 146 | (interactive) 147 | (if (not (and matlab-server-process 148 | (process-live-p matlab-server-process))) 149 | (progn 150 | (message "matlab: starting background matlab...") 151 | (matlab-send-request-sync-helper "import com;") ;; ignore first request 152 | (setq matlab-just-startup nil) 153 | (message "matlab: done") 154 | (if (and (or (string= "matlab-mode" major-mode) 155 | (string= (buffer-name) "*MATLAB*")) 156 | (not (string= matlab-last-work-directory default-directory))) 157 | (progn (matlab-send-request-sync (concat "cd " default-directory)) 158 | (setq matlab-last-work-directory default-directory)))))) 159 | 160 | 161 | (defun matlab-send-request-sync (request &rest args) 162 | "Send a request to matlab and wait for the result." 163 | (if matlab-process-auto-start 164 | (if (or matlab-just-startup (not (process-live-p matlab-server-process))) 165 | (progn 166 | (message "matlab: starting background matlab...") 167 | (matlab-send-request-sync-helper "import com;") ;; ignore first request 168 | (setq matlab-just-startup nil) 169 | (apply 'matlab-send-request-sync-helper request args) 170 | (message "matlab: done")) 171 | (apply 'matlab-send-request-sync-helper request args)))) 172 | 173 | 174 | (provide 'matlab-server) 175 | --------------------------------------------------------------------------------