|
| 1 | +;;; cython-mode.el --- Major mode for editing Cython files |
| 2 | + |
| 3 | +;; License: Apache-2.0 |
| 4 | + |
| 5 | +;;; Commentary: |
| 6 | + |
| 7 | +;; This should work with python-mode.el as well as either the new |
| 8 | +;; python.el or the old. |
| 9 | + |
| 10 | +;;; Code: |
| 11 | + |
| 12 | +;; Load python-mode if available, otherwise use builtin emacs python package |
| 13 | +(when (not (require 'python-mode nil t)) |
| 14 | + (require 'python)) |
| 15 | +(eval-when-compile (require 'rx)) |
| 16 | + |
| 17 | +;;;###autoload |
| 18 | +(add-to-list 'auto-mode-alist '("\\.pyx\\'" . cython-mode)) |
| 19 | +;;;###autoload |
| 20 | +(add-to-list 'auto-mode-alist '("\\.pxd\\'" . cython-mode)) |
| 21 | +;;;###autoload |
| 22 | +(add-to-list 'auto-mode-alist '("\\.pxi\\'" . cython-mode)) |
| 23 | + |
| 24 | + |
| 25 | +(defvar cython-buffer nil |
| 26 | + "Variable pointing to the cython buffer which was compiled.") |
| 27 | + |
| 28 | +(defun cython-compile () |
| 29 | + "Compile the file via Cython." |
| 30 | + (interactive) |
| 31 | + (let ((cy-buffer (current-buffer))) |
| 32 | + (with-current-buffer |
| 33 | + (compile compile-command) |
| 34 | + (set (make-local-variable 'cython-buffer) cy-buffer) |
| 35 | + (add-to-list (make-local-variable 'compilation-finish-functions) |
| 36 | + 'cython-compilation-finish)))) |
| 37 | + |
| 38 | +(defun cython-compilation-finish (buffer how) |
| 39 | + "Called when Cython compilation finishes." |
| 40 | + ;; XXX could annotate source here |
| 41 | + ) |
| 42 | + |
| 43 | +(defvar cython-mode-map |
| 44 | + (let ((map (make-sparse-keymap))) |
| 45 | + ;; Will inherit from `python-mode-map' thanks to define-derived-mode. |
| 46 | + (define-key map "\C-c\C-c" 'cython-compile) |
| 47 | + map) |
| 48 | + "Keymap used in `cython-mode'.") |
| 49 | + |
| 50 | +(defvar cython-font-lock-keywords |
| 51 | + `(;; ctypedef statement: "ctypedef (...type... alias)?" |
| 52 | + (,(rx |
| 53 | + ;; keyword itself |
| 54 | + symbol-start (group "ctypedef") |
| 55 | + ;; type specifier: at least 1 non-identifier symbol + 1 identifier |
| 56 | + ;; symbol and anything but a comment-starter after that. |
| 57 | + (opt (regexp "[^a-zA-Z0-9_\n]+[a-zA-Z0-9_][^#\n]*") |
| 58 | + ;; type alias: an identifier |
| 59 | + symbol-start (group (regexp "[a-zA-Z_]+[a-zA-Z0-9_]*")) |
| 60 | + ;; space-or-comments till the end of the line |
| 61 | + (* space) (opt "#" (* nonl)) line-end)) |
| 62 | + (1 font-lock-keyword-face) |
| 63 | + (2 font-lock-type-face nil 'noerror)) |
| 64 | + ;; new keywords in Cython language |
| 65 | + (,(rx symbol-start |
| 66 | + (or "by" "cdef" "cimport" "cpdef" |
| 67 | + "extern" "gil" "include" "nogil" "property" "public" |
| 68 | + "readonly" "DEF" "IF" "ELIF" "ELSE" |
| 69 | + "new" "del" "cppclass" "namespace" "const" |
| 70 | + "__stdcall" "__cdecl" "__fastcall" "inline" "api") |
| 71 | + symbol-end) |
| 72 | + . font-lock-keyword-face) |
| 73 | + ;; Question mark won't match at a symbol-end, so 'except?' must be |
| 74 | + ;; special-cased. It's simpler to handle it separately than weaving it |
| 75 | + ;; into the lengthy list of other keywords. |
| 76 | + (,(rx symbol-start "except?") . font-lock-keyword-face) |
| 77 | + ;; C and Python types (highlight as builtins) |
| 78 | + (,(rx symbol-start |
| 79 | + (or |
| 80 | + "object" "dict" "list" |
| 81 | + ;; basic c type names |
| 82 | + "void" "char" "int" "float" "double" "bint" |
| 83 | + ;; longness/signed/constness |
| 84 | + "signed" "unsigned" "long" "short" |
| 85 | + ;; special basic c types |
| 86 | + "size_t" "Py_ssize_t" "Py_UNICODE" "Py_UCS4" "ssize_t" "ptrdiff_t") |
| 87 | + symbol-end) |
| 88 | + . font-lock-builtin-face) |
| 89 | + (,(rx symbol-start "NULL" symbol-end) |
| 90 | + . font-lock-constant-face) |
| 91 | + ;; cdef is used for more than functions, so simply highlighting the next |
| 92 | + ;; word is problematic. struct, enum and property work though. |
| 93 | + (,(rx symbol-start |
| 94 | + (group (or "struct" "enum" "union" |
| 95 | + (seq "ctypedef" (+ space "fused")))) |
| 96 | + (+ space) (group (regexp "[a-zA-Z_]+[a-zA-Z0-9_]*"))) |
| 97 | + (1 font-lock-keyword-face prepend) (2 font-lock-type-face)) |
| 98 | + ("\\_<property[ \t]+\\([a-zA-Z_]+[a-zA-Z0-9_]*\\)" |
| 99 | + 1 font-lock-function-name-face)) |
| 100 | + "Additional font lock keywords for Cython mode.") |
| 101 | + |
| 102 | +;;;###autoload |
| 103 | +(defgroup cython nil "Major mode for editing and compiling Cython files" |
| 104 | + :group 'languages |
| 105 | + :prefix "cython-" |
| 106 | + :link '(url-link :tag "Homepage" "https://cython.org/")) |
| 107 | + |
| 108 | +;;;###autoload |
| 109 | +(defcustom cython-default-compile-format "cython -a %s" |
| 110 | + "Format for the default command to compile a Cython file. |
| 111 | +It will be passed to `format' with `buffer-file-name' as the only other argument." |
| 112 | + :group 'cython |
| 113 | + :type 'string) |
| 114 | + |
| 115 | +;; Some functions defined differently in the different python modes |
| 116 | +(defun cython-comment-line-p () |
| 117 | + "Return non-nil if current line is a comment." |
| 118 | + (save-excursion |
| 119 | + (back-to-indentation) |
| 120 | + (eq ?# (char-after (point))))) |
| 121 | + |
| 122 | +(defun cython-in-string/comment () |
| 123 | + "Return non-nil if point is in a comment or string." |
| 124 | + (nth 8 (syntax-ppss))) |
| 125 | + |
| 126 | +(defalias 'cython-beginning-of-statement |
| 127 | + (cond |
| 128 | + ;; python-mode.el |
| 129 | + ((fboundp 'py-beginning-of-statement) |
| 130 | + 'py-beginning-of-statement) |
| 131 | + ;; old python.el |
| 132 | + ((fboundp 'python-beginning-of-statement) |
| 133 | + 'python-beginning-of-statement) |
| 134 | + ;; new python.el |
| 135 | + ((fboundp 'python-nav-beginning-of-statement) |
| 136 | + 'python-nav-beginning-of-statement) |
| 137 | + (t (error "Couldn't find implementation for `cython-beginning-of-statement'")))) |
| 138 | + |
| 139 | +(defalias 'cython-beginning-of-block |
| 140 | + (cond |
| 141 | + ;; python-mode.el |
| 142 | + ((fboundp 'py-beginning-of-block) |
| 143 | + 'py-beginning-of-block) |
| 144 | + ;; old python.el |
| 145 | + ((fboundp 'python-beginning-of-block) |
| 146 | + 'python-beginning-of-block) |
| 147 | + ;; new python.el |
| 148 | + ((fboundp 'python-nav-beginning-of-block) |
| 149 | + 'python-nav-beginning-of-block) |
| 150 | + (t (error "Couldn't find implementation for `cython-beginning-of-block'")))) |
| 151 | + |
| 152 | +(defalias 'cython-end-of-statement |
| 153 | + (cond |
| 154 | + ;; python-mode.el |
| 155 | + ((fboundp 'py-end-of-statement) |
| 156 | + 'py-end-of-statement) |
| 157 | + ;; old python.el |
| 158 | + ((fboundp 'python-end-of-statement) |
| 159 | + 'python-end-of-statement) |
| 160 | + ;; new python.el |
| 161 | + ((fboundp 'python-nav-end-of-statement) |
| 162 | + 'python-nav-end-of-statement) |
| 163 | + (t (error "Couldn't find implementation for `cython-end-of-statement'")))) |
| 164 | + |
| 165 | +(defun cython-open-block-statement-p (&optional bos) |
| 166 | + "Return non-nil if statement at point opens a Cython block. |
| 167 | +BOS non-nil means point is known to be at beginning of statement." |
| 168 | + (save-excursion |
| 169 | + (unless bos (cython-beginning-of-statement)) |
| 170 | + (looking-at (rx (and (or "if" "else" "elif" "while" "for" "def" "cdef" "cpdef" |
| 171 | + "class" "try" "except" "finally" "with" |
| 172 | + "EXAMPLES:" "TESTS:" "INPUT:" "OUTPUT:") |
| 173 | + symbol-end))))) |
| 174 | + |
| 175 | +(defun cython-beginning-of-defun () |
| 176 | + "`beginning-of-defun-function' for Cython. |
| 177 | +Finds beginning of innermost nested class or method definition. |
| 178 | +Returns the name of the definition found at the end, or nil if |
| 179 | +reached start of buffer." |
| 180 | + (let ((ci (current-indentation)) |
| 181 | + (def-re (rx line-start (0+ space) (or "def" "cdef" "cpdef" "class") (1+ space) |
| 182 | + (group (1+ (or word (syntax symbol)))))) |
| 183 | + found lep) ;; def-line |
| 184 | + (if (cython-comment-line-p) |
| 185 | + (setq ci most-positive-fixnum)) |
| 186 | + (while (and (not (bobp)) (not found)) |
| 187 | + ;; Treat bol at beginning of function as outside function so |
| 188 | + ;; that successive C-M-a makes progress backwards. |
| 189 | + ;;(setq def-line (looking-at def-re)) |
| 190 | + (unless (bolp) (end-of-line)) |
| 191 | + (setq lep (line-end-position)) |
| 192 | + (if (and (re-search-backward def-re nil 'move) |
| 193 | + ;; Must be less indented or matching top level, or |
| 194 | + ;; equally indented if we started on a definition line. |
| 195 | + (let ((in (current-indentation))) |
| 196 | + (or (and (zerop ci) (zerop in)) |
| 197 | + (= lep (line-end-position)) ; on initial line |
| 198 | + ;; Not sure why it was like this -- fails in case of |
| 199 | + ;; last internal function followed by first |
| 200 | + ;; non-def statement of the main body. |
| 201 | + ;;(and def-line (= in ci)) |
| 202 | + (= in ci) |
| 203 | + (< in ci))) |
| 204 | + (not (cython-in-string/comment))) |
| 205 | + (setq found t))))) |
| 206 | + |
| 207 | +(defun cython-end-of-defun () |
| 208 | + "`end-of-defun-function' for Cython. |
| 209 | +Finds end of innermost nested class or method definition." |
| 210 | + (let ((orig (point)) |
| 211 | + (pattern (rx line-start (0+ space) (or "def" "cdef" "cpdef" "class") space))) |
| 212 | + ;; Go to start of current block and check whether it's at top |
| 213 | + ;; level. If it is, and not a block start, look forward for |
| 214 | + ;; definition statement. |
| 215 | + (when (cython-comment-line-p) |
| 216 | + (end-of-line) |
| 217 | + (forward-comment most-positive-fixnum)) |
| 218 | + (when (not (cython-open-block-statement-p)) |
| 219 | + (cython-beginning-of-block)) |
| 220 | + (if (zerop (current-indentation)) |
| 221 | + (unless (cython-open-block-statement-p) |
| 222 | + (while (and (re-search-forward pattern nil 'move) |
| 223 | + (cython-in-string/comment))) ; just loop |
| 224 | + (unless (eobp) |
| 225 | + (beginning-of-line))) |
| 226 | + ;; Don't move before top-level statement that would end defun. |
| 227 | + (end-of-line) |
| 228 | + (beginning-of-defun)) |
| 229 | + ;; If we got to the start of buffer, look forward for |
| 230 | + ;; definition statement. |
| 231 | + (when (and (bobp) (not (looking-at (rx (or "def" "cdef" "cpdef" "class"))))) |
| 232 | + (while (and (not (eobp)) |
| 233 | + (re-search-forward pattern nil 'move) |
| 234 | + (cython-in-string/comment)))) ; just loop |
| 235 | + ;; We're at a definition statement (or end-of-buffer). |
| 236 | + ;; This is where we should have started when called from end-of-defun |
| 237 | + (unless (eobp) |
| 238 | + (let ((block-indentation (current-indentation))) |
| 239 | + (python-nav-end-of-statement) |
| 240 | + (while (and (forward-line 1) |
| 241 | + (not (eobp)) |
| 242 | + (or (and (> (current-indentation) block-indentation) |
| 243 | + (or (cython-end-of-statement) t)) |
| 244 | + ;; comment or empty line |
| 245 | + (looking-at (rx (0+ space) (or eol "#")))))) |
| 246 | + (forward-comment -1)) |
| 247 | + ;; Count trailing space in defun (but not trailing comments). |
| 248 | + (skip-syntax-forward " >") |
| 249 | + (unless (eobp) ; e.g. missing final newline |
| 250 | + (beginning-of-line))) |
| 251 | + ;; Catch pathological cases like this, where the beginning-of-defun |
| 252 | + ;; skips to a definition we're not in: |
| 253 | + ;; if ...: |
| 254 | + ;; ... |
| 255 | + ;; else: |
| 256 | + ;; ... # point here |
| 257 | + ;; ... |
| 258 | + ;; def ... |
| 259 | + (if (< (point) orig) |
| 260 | + (goto-char (point-max))))) |
| 261 | + |
| 262 | +(defun cython-current-defun () |
| 263 | + "`add-log-current-defun-function' for Cython." |
| 264 | + (save-excursion |
| 265 | + ;; Move up the tree of nested `class' and `def' blocks until we |
| 266 | + ;; get to zero indentation, accumulating the defined names. |
| 267 | + (let ((not-finished t) |
| 268 | + accum) |
| 269 | + (skip-chars-backward " \t\r\n") |
| 270 | + (cython-beginning-of-defun) |
| 271 | + (while not-finished |
| 272 | + (beginning-of-line) |
| 273 | + (skip-chars-forward " \t\r\n") |
| 274 | + (if (looking-at (rx (0+ space) (or "def" "class") (1+ space) |
| 275 | + (group (1+ (or word (syntax symbol)))))) |
| 276 | + (push (match-string 1) accum) |
| 277 | + (if (looking-at (rx (0+ space) (or "cdef" "cpdef") (1+ space) (1+ word) (1+ space) |
| 278 | + (group (1+ (or word (syntax symbol)))))) |
| 279 | + (push (match-string 1) accum))) |
| 280 | + (let ((indentation (current-indentation))) |
| 281 | + (if (= 0 indentation) |
| 282 | + (setq not-finished nil) |
| 283 | + (while (= indentation (current-indentation)) |
| 284 | + (message "%s %s" indentation (current-indentation)) |
| 285 | + (cython-beginning-of-defun))))) |
| 286 | + (if accum (mapconcat 'identity accum "."))))) |
| 287 | + |
| 288 | +;;;###autoload |
| 289 | +(define-derived-mode cython-mode python-mode "Cython" |
| 290 | + "Major mode for Cython development, derived from Python mode. |
| 291 | +
|
| 292 | +\\{cython-mode-map}" |
| 293 | + (font-lock-add-keywords nil cython-font-lock-keywords) |
| 294 | + (set (make-local-variable 'outline-regexp) |
| 295 | + (rx (* space) (or "class" "def" "cdef" "cpdef" "elif" "else" "except" "finally" |
| 296 | + "for" "if" "try" "while" "with") |
| 297 | + symbol-end)) |
| 298 | + (set (make-local-variable 'beginning-of-defun-function) |
| 299 | + #'cython-beginning-of-defun) |
| 300 | + (set (make-local-variable 'end-of-defun-function) |
| 301 | + #'cython-end-of-defun) |
| 302 | + (set (make-local-variable 'compile-command) |
| 303 | + (format cython-default-compile-format (shell-quote-argument (or buffer-file-name "")))) |
| 304 | + (set (make-local-variable 'add-log-current-defun-function) |
| 305 | + #'cython-current-defun) |
| 306 | + (add-hook 'which-func-functions #'cython-current-defun nil t) |
| 307 | + (add-to-list (make-local-variable 'compilation-finish-functions) |
| 308 | + 'cython-compilation-finish)) |
| 309 | + |
| 310 | +(provide 'cython-mode) |
| 311 | + |
| 312 | +;;; cython-mode.el ends here |
0 commit comments