summaryrefslogtreecommitdiff
path: root/Tools/cython-mode.el
blob: e4be99f5b9cad6429c936b6206542db4026da199 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
;;; cython-mode.el --- Major mode for editing Cython files

;; License: Apache-2.0

;;; Commentary:

;; This should work with python-mode.el as well as either the new
;; python.el or the old.

;;; Code:

;; Load python-mode if available, otherwise use builtin emacs python package
(when (not (require 'python-mode nil t))
  (require 'python))
(eval-when-compile (require 'rx))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.pyx\\'" . cython-mode))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.pxd\\'" . cython-mode))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.pxi\\'" . cython-mode))


(defvar cython-buffer nil
  "Variable pointing to the cython buffer which was compiled.")

(defun cython-compile ()
  "Compile the file via Cython."
  (interactive)
  (let ((cy-buffer (current-buffer)))
    (with-current-buffer
        (compile compile-command)
      (set (make-local-variable 'cython-buffer) cy-buffer)
      (add-to-list (make-local-variable 'compilation-finish-functions)
                   'cython-compilation-finish))))

(defun cython-compilation-finish (buffer how)
  "Called when Cython compilation finishes."
  ;; XXX could annotate source here
  )

(defvar cython-mode-map
  (let ((map (make-sparse-keymap)))
    ;; Will inherit from `python-mode-map' thanks to define-derived-mode.
    (define-key map "\C-c\C-c" 'cython-compile)
    map)
  "Keymap used in `cython-mode'.")

(defvar cython-font-lock-keywords
  `(;; ctypedef statement: "ctypedef (...type... alias)?"
    (,(rx
       ;; keyword itself
       symbol-start (group "ctypedef")
       ;; type specifier: at least 1 non-identifier symbol + 1 identifier
       ;; symbol and anything but a comment-starter after that.
       (opt (regexp "[^a-zA-Z0-9_\n]+[a-zA-Z0-9_][^#\n]*")
            ;; type alias: an identifier
            symbol-start (group (regexp "[a-zA-Z_]+[a-zA-Z0-9_]*"))
            ;; space-or-comments till the end of the line
            (* space) (opt "#" (* nonl)) line-end))
     (1 font-lock-keyword-face)
     (2 font-lock-type-face nil 'noerror))
    ;; new keywords in Cython language
    (,(rx symbol-start
          (or "by" "cdef" "cimport" "cpdef"
              "extern" "gil" "include" "nogil" "property" "public"
              "readonly" "DEF" "IF" "ELIF" "ELSE"
              "new" "del" "cppclass" "namespace" "const"
              "__stdcall" "__cdecl" "__fastcall" "inline" "api")
          symbol-end)
     . font-lock-keyword-face)
    ;; Question mark won't match at a symbol-end, so 'except?' must be
    ;; special-cased.  It's simpler to handle it separately than weaving it
    ;; into the lengthy list of other keywords.
    (,(rx symbol-start "except?") . font-lock-keyword-face)
    ;; C and Python types (highlight as builtins)
    (,(rx symbol-start
          (or
           "object" "dict" "list"
           ;; basic c type names
           "void" "char" "int" "float" "double" "bint"
           ;; longness/signed/constness
           "signed" "unsigned" "long" "short"
           ;; special basic c types
           "size_t" "Py_ssize_t" "Py_UNICODE" "Py_UCS4" "ssize_t" "ptrdiff_t")
          symbol-end)
     . font-lock-builtin-face)
    (,(rx symbol-start "NULL" symbol-end)
     . font-lock-constant-face)
    ;; cdef is used for more than functions, so simply highlighting the next
    ;; word is problematic. struct, enum and property work though.
    (,(rx symbol-start
          (group (or "struct" "enum" "union"
                     (seq "ctypedef" (+ space "fused"))))
          (+ space) (group (regexp "[a-zA-Z_]+[a-zA-Z0-9_]*")))
     (1 font-lock-keyword-face prepend) (2 font-lock-type-face))
    ("\\_<property[ \t]+\\([a-zA-Z_]+[a-zA-Z0-9_]*\\)"
     1 font-lock-function-name-face))
  "Additional font lock keywords for Cython mode.")

;;;###autoload
(defgroup cython nil "Major mode for editing and compiling Cython files"
  :group 'languages
  :prefix "cython-"
  :link '(url-link :tag "Homepage" "http://cython.org"))

;;;###autoload
(defcustom cython-default-compile-format "cython -a %s"
  "Format for the default command to compile a Cython file.
It will be passed to `format' with `buffer-file-name' as the only other argument."
  :group 'cython
  :type 'string)

;; Some functions defined differently in the different python modes
(defun cython-comment-line-p ()
  "Return non-nil if current line is a comment."
  (save-excursion
    (back-to-indentation)
    (eq ?# (char-after (point)))))

(defun cython-in-string/comment ()
  "Return non-nil if point is in a comment or string."
  (nth 8 (syntax-ppss)))

(defalias 'cython-beginning-of-statement
  (cond
   ;; python-mode.el
   ((fboundp 'py-beginning-of-statement)
    'py-beginning-of-statement)
   ;; old python.el
   ((fboundp 'python-beginning-of-statement)
    'python-beginning-of-statement)
   ;; new python.el
   ((fboundp 'python-nav-beginning-of-statement)
    'python-nav-beginning-of-statement)
   (t (error "Couldn't find implementation for `cython-beginning-of-statement'"))))

(defalias 'cython-beginning-of-block
  (cond
   ;; python-mode.el
   ((fboundp 'py-beginning-of-block)
    'py-beginning-of-block)
   ;; old python.el
   ((fboundp 'python-beginning-of-block)
    'python-beginning-of-block)
   ;; new python.el
   ((fboundp 'python-nav-beginning-of-block)
    'python-nav-beginning-of-block)
   (t (error "Couldn't find implementation for `cython-beginning-of-block'"))))

(defalias 'cython-end-of-statement
  (cond
   ;; python-mode.el
   ((fboundp 'py-end-of-statement)
    'py-end-of-statement)
   ;; old python.el
   ((fboundp 'python-end-of-statement)
    'python-end-of-statement)
   ;; new python.el
   ((fboundp 'python-nav-end-of-statement)
    'python-nav-end-of-statement)
   (t (error "Couldn't find implementation for `cython-end-of-statement'"))))

(defun cython-open-block-statement-p (&optional bos)
  "Return non-nil if statement at point opens a Cython block.
BOS non-nil means point is known to be at beginning of statement."
  (save-excursion
    (unless bos (cython-beginning-of-statement))
    (looking-at (rx (and (or "if" "else" "elif" "while" "for" "def" "cdef" "cpdef"
                             "class" "try" "except" "finally" "with"
                             "EXAMPLES:" "TESTS:" "INPUT:" "OUTPUT:")
                         symbol-end)))))

(defun cython-beginning-of-defun ()
  "`beginning-of-defun-function' for Cython.
Finds beginning of innermost nested class or method definition.
Returns the name of the definition found at the end, or nil if
reached start of buffer."
  (let ((ci (current-indentation))
        (def-re (rx line-start (0+ space) (or "def" "cdef" "cpdef" "class") (1+ space)
                    (group (1+ (or word (syntax symbol))))))
        found lep) ;; def-line
    (if (cython-comment-line-p)
        (setq ci most-positive-fixnum))
    (while (and (not (bobp)) (not found))
      ;; Treat bol at beginning of function as outside function so
      ;; that successive C-M-a makes progress backwards.
      ;;(setq def-line (looking-at def-re))
      (unless (bolp) (end-of-line))
      (setq lep (line-end-position))
      (if (and (re-search-backward def-re nil 'move)
               ;; Must be less indented or matching top level, or
               ;; equally indented if we started on a definition line.
               (let ((in (current-indentation)))
                 (or (and (zerop ci) (zerop in))
                     (= lep (line-end-position)) ; on initial line
                     ;; Not sure why it was like this -- fails in case of
                     ;; last internal function followed by first
                     ;; non-def statement of the main body.
                     ;;(and def-line (= in ci))
                     (= in ci)
                     (< in ci)))
               (not (cython-in-string/comment)))
          (setq found t)))))

(defun cython-end-of-defun ()
  "`end-of-defun-function' for Cython.
Finds end of innermost nested class or method definition."
  (let ((orig (point))
        (pattern (rx line-start (0+ space) (or "def" "cdef" "cpdef" "class") space)))
    ;; Go to start of current block and check whether it's at top
    ;; level.  If it is, and not a block start, look forward for
    ;; definition statement.
    (when (cython-comment-line-p)
      (end-of-line)
      (forward-comment most-positive-fixnum))
    (when (not (cython-open-block-statement-p))
      (cython-beginning-of-block))
    (if (zerop (current-indentation))
        (unless (cython-open-block-statement-p)
          (while (and (re-search-forward pattern nil 'move)
                      (cython-in-string/comment))) ; just loop
          (unless (eobp)
            (beginning-of-line)))
      ;; Don't move before top-level statement that would end defun.
      (end-of-line)
      (beginning-of-defun))
    ;; If we got to the start of buffer, look forward for
    ;; definition statement.
    (when (and (bobp) (not (looking-at (rx (or "def" "cdef" "cpdef" "class")))))
      (while (and (not (eobp))
                  (re-search-forward pattern nil 'move)
                  (cython-in-string/comment)))) ; just loop
    ;; We're at a definition statement (or end-of-buffer).
    ;; This is where we should have started when called from end-of-defun
    (unless (eobp)
      (let ((block-indentation (current-indentation)))
        (python-nav-end-of-statement)
        (while (and (forward-line 1)
                    (not (eobp))
                    (or (and (> (current-indentation) block-indentation)
                             (or (cython-end-of-statement) t))
                        ;; comment or empty line
                        (looking-at (rx (0+ space) (or eol "#"))))))
        (forward-comment -1))
      ;; Count trailing space in defun (but not trailing comments).
      (skip-syntax-forward " >")
      (unless (eobp)			; e.g. missing final newline
        (beginning-of-line)))
    ;; Catch pathological cases like this, where the beginning-of-defun
    ;; skips to a definition we're not in:
    ;; if ...:
    ;;     ...
    ;; else:
    ;;     ...  # point here
    ;;     ...
    ;;     def ...
    (if (< (point) orig)
        (goto-char (point-max)))))

(defun cython-current-defun ()
  "`add-log-current-defun-function' for Cython."
  (save-excursion
    ;; Move up the tree of nested `class' and `def' blocks until we
    ;; get to zero indentation, accumulating the defined names.
    (let ((start t)
          accum)
      (while (or start (> (current-indentation) 0))
        (setq start nil)
        (cython-beginning-of-block)
        (end-of-line)
        (beginning-of-defun)
        (if (looking-at (rx (0+ space) (or "def" "cdef" "cpdef" "class") (1+ space)
                            (group (1+ (or word (syntax symbol))))))
            (push (match-string 1) accum)))
      (if accum (mapconcat 'identity accum ".")))))

;;;###autoload
(define-derived-mode cython-mode python-mode "Cython"
  "Major mode for Cython development, derived from Python mode.

\\{cython-mode-map}"
  (font-lock-add-keywords nil cython-font-lock-keywords)
  (set (make-local-variable 'outline-regexp)
       (rx (* space) (or "class" "def" "cdef" "cpdef" "elif" "else" "except" "finally"
                         "for" "if" "try" "while" "with")
           symbol-end))
  (set (make-local-variable 'beginning-of-defun-function)
       #'cython-beginning-of-defun)
  (set (make-local-variable 'end-of-defun-function)
       #'cython-end-of-defun)
  (set (make-local-variable 'compile-command)
       (format cython-default-compile-format (shell-quote-argument (or buffer-file-name ""))))
  (set (make-local-variable 'add-log-current-defun-function)
       #'cython-current-defun)
  (add-hook 'which-func-functions #'cython-current-defun nil t)
  (add-to-list (make-local-variable 'compilation-finish-functions)
               'cython-compilation-finish))

(provide 'cython-mode)

;;; cython-mode.el ends here