fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 22h ago
526 lines · 17.8 KB Raw Blame History
1 ;;; zig-mode.el --- A major mode for the Zig programming language -*- lexical-binding: t -*-
2
3 ;; Author: Andrea Orru <andreaorru1991@gmail.com>
4 ;; Andrew Kelley <superjoe30@gmail.com>
5 ;; Maintainer: Shen, Jen-Chieh <jcs090218@gmail.com>
6 ;; URL: https://github.com/zig-lang/zig-mode
7 ;; Version: 0.0.8
8 ;; Package-Requires: ((emacs "26.1") (reformatter "0.6"))
9 ;; Keywords: zig, languages
10
11 ;; This file is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 3, or (at your option)
14 ;; any later version.
15
16 ;; This file is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
20
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
23
24 ;;; Commentary:
25
26 ;; A major mode for the Zig programming languages.
27
28 ;; See documentation on https://github.com/zig-lang/zig-mode
29
30 ;;; Code:
31
32 (require 'reformatter)
33
34 (defgroup zig-mode nil
35 "Support for Zig code."
36 :link '(url-link "https://ziglang.org/")
37 :group 'languages)
38
39 (defcustom zig-indent-offset 4
40 "Indent Zig code by this number of spaces."
41 :type 'integer
42 :group 'zig-mode
43 :safe #'integerp)
44
45 (defcustom zig-format-on-save t
46 "Format buffers before saving using zig fmt."
47 :type 'boolean
48 :safe #'booleanp
49 :group 'zig-mode)
50
51 (defcustom zig-format-show-buffer t
52 "Show a *zig-fmt* buffer after zig fmt completes with errors"
53 :type 'boolean
54 :safe #'booleanp
55 :group 'zig-mode)
56
57 (defcustom zig-zig-bin "zig"
58 "Path to zig executable."
59 :type 'file
60 :safe #'stringp
61 :group 'zig-mode)
62
63 (defcustom zig-run-optimization-mode "Debug"
64 "Optimization mode to run code with."
65 :type 'string
66 :safe #'stringp
67 :group 'zig-mode)
68
69 (defcustom zig-test-optimization-mode "Debug"
70 "Optimization mode to run tests with."
71 :type 'string
72 :safe #'stringp
73 :group 'zig-mode)
74
75 ;; zig CLI commands
76
77 (defun zig--run-cmd (cmd &optional source &rest args)
78 "Use compile command to execute a zig CMD with ARGS if given.
79 If given a SOURCE, execute the CMD on it."
80 (let ((cmd-args
81 (if source
82 (mapconcat 'shell-quote-argument (cons source args) " ")
83 args)))
84 (compilation-start (concat zig-zig-bin " " cmd " " cmd-args))))
85
86 ;;;###autoload
87 (defun zig-compile ()
88 "Compile using `zig build`."
89 (interactive)
90 (zig--run-cmd "build"))
91
92 ;;;###autoload
93 (defun zig-build-exe ()
94 "Create executable from source or object file."
95 (interactive)
96 (zig--run-cmd "build-exe" (file-local-name (buffer-file-name))))
97
98 ;;;###autoload
99 (defun zig-build-lib ()
100 "Create library from source or assembly."
101 (interactive)
102 (zig--run-cmd "build-lib" (file-local-name (buffer-file-name))))
103
104 ;;;###autoload
105 (defun zig-build-obj ()
106 "Create object from source or assembly."
107 (interactive)
108 (zig--run-cmd "build-obj" (file-local-name (buffer-file-name))))
109
110 ;;;###autoload
111 (defun zig-test-buffer ()
112 "Test buffer using `zig test`."
113 (interactive)
114 (zig--run-cmd "test" (file-local-name (buffer-file-name)) "-O" zig-test-optimization-mode))
115
116 ;;;###autoload
117 (defun zig-run ()
118 "Create an executable from the current buffer and run it immediately."
119 (interactive)
120 (zig--run-cmd "run" (file-local-name (buffer-file-name)) "-O" zig-run-optimization-mode))
121
122 ;; zig fmt
123
124 (reformatter-define zig-format
125 :program zig-zig-bin
126 :args '("fmt" "--stdin")
127 :group 'zig-mode
128 :lighter " ZigFmt")
129
130 ;;;###autoload (autoload 'zig-format-buffer "current-file" nil t)
131 ;;;###autoload (autoload 'zig-format-region "current-file" nil t)
132 ;;;###autoload (autoload 'zig-format-on-save-mode "current-file" nil t)
133
134 (defun zig-re-word (inner)
135 "Construct a regular expression for the word INNER."
136 (concat "\\<" inner "\\>"))
137
138 (defun zig-re-grab (inner)
139 "Construct a group regular expression for INNER."
140 (concat "\\(" inner "\\)"))
141
142 (defconst zig-re-optional "\\(?:[[:space:]]*\\?[[:space:]]*\\)")
143 (defconst zig-re-pointer "\\(?:[[:space:]]*\\*\\(?:const[[:space:]]*\\)?[[:space:]]*\\)")
144 (defconst zig-re-array "\\(?:[[:space:]]*\\[[^]]*\\]\\(?:const[[:space:]]*\\)?[[:space:]]*\\)")
145
146 (defconst zig-re-optionals-pointers-arrays
147 (concat "\\(?:" zig-re-optional "\\|" zig-re-pointer "\\|" zig-re-array "\\)*"))
148
149 (defconst zig-re-identifier "[[:word:]_][[:word:]_[:digit:]]*")
150 (defconst zig-re-type-annotation
151 (concat (zig-re-grab zig-re-identifier)
152 "[[:space:]]*:[[:space:]]*"
153 zig-re-optionals-pointers-arrays
154 (zig-re-grab zig-re-identifier)))
155
156 (defun zig-re-definition (dtype)
157 "Construct a regular expression for definitions of type DTYPE."
158 (concat (zig-re-word dtype) "[[:space:]]+" (zig-re-grab zig-re-identifier)))
159
160 (defconst zig-mode-syntax-table
161 (let ((table (make-syntax-table)))
162
163 ;; Operators
164 (dolist (i '(?+ ?- ?* ?/ ?% ?& ?| ?= ?! ?< ?>))
165 (modify-syntax-entry i "." table))
166
167 ;; Strings
168 (modify-syntax-entry ?\' "\"" table)
169 (modify-syntax-entry ?\" "\"" table)
170 (modify-syntax-entry ?\\ "\\" table)
171
172 ;; Comments
173 (modify-syntax-entry ?/ ". 12" table)
174 (modify-syntax-entry ?\n ">" table)
175
176 table))
177
178 (defconst zig-keywords
179 '(
180 ;; Storage
181 "const" "var" "extern" "packed" "export" "pub" "noalias" "inline"
182 "noinline" "comptime" "callconv" "volatile" "allowzero"
183 "align" "linksection" "threadlocal" "addrspace"
184
185 ;; Structure
186 "struct" "enum" "union" "error" "opaque"
187
188 ;; Statement
189 "break" "return" "continue" "asm" "defer" "errdefer" "unreachable"
190 "try" "catch" "async" "nosuspend" "await" "suspend" "resume"
191
192 ;; Conditional
193 "if" "else" "switch" "and" "or" "orelse"
194
195 ;; Repeat
196 "while" "for"
197
198 ;; Other keywords
199 "fn" "usingnamespace" "test"))
200
201 (defconst zig-types
202 '(
203 ;; Integer types
204 "i2" "u2" "i3" "u3" "i4" "u4" "i5" "u5" "i6" "u6" "i7" "u7" "i8" "u8"
205 "i16" "u16" "i29" "u29" "i32" "u32" "i64" "u64" "i128" "u128"
206 "isize" "usize"
207
208 ;; Floating types
209 "f16" "f32" "f64" "f80" "f128"
210
211 ;; C types
212 "c_char" "c_short" "c_ushort" "c_int" "c_uint" "c_long" "c_ulong"
213 "c_longlong" "c_ulonglong" "c_longdouble"
214
215 ;; Comptime types
216 "comptime_int" "comptime_float"
217
218 ;; Other types
219 "bool" "void" "noreturn" "type" "error" "anyerror" "anyframe" "anytype"
220 "anyopaque"))
221
222 (defconst zig-constants
223 '(
224 ;; Boolean
225 "true" "false"
226
227 ;; Other constants
228 "null" "undefined"))
229
230 (defconst zig-electric-indent-chars
231 '(?\; ?, ?\) ?\] ?}))
232
233
234 (defface zig-multiline-string-face
235 '((t :inherit font-lock-string-face))
236 "Face for multiline string literals."
237 :group 'zig-mode)
238
239 (defvar zig-font-lock-keywords
240 (append
241 `(
242 ;; Builtins (prefixed with @)
243 (,(concat "@" zig-re-identifier) . font-lock-builtin-face)
244
245 ;; Keywords, constants and types
246 (,(regexp-opt zig-keywords 'symbols) . font-lock-keyword-face)
247 (,(regexp-opt zig-constants 'symbols) . font-lock-constant-face)
248 (,(regexp-opt zig-types 'symbols) . font-lock-type-face)
249
250 ;; Type annotations (both variable and type)
251 (,zig-re-type-annotation 1 font-lock-variable-name-face)
252 (,zig-re-type-annotation 2 font-lock-type-face)
253 )
254
255 ;; Definitions
256 (mapcar (lambda (x)
257 (list (zig-re-definition (car x))
258 1 (cdr x)))
259 '(("const" . font-lock-variable-name-face)
260 ("var" . font-lock-variable-name-face)
261 ("fn" . font-lock-function-name-face)))))
262
263 (defun zig-paren-nesting-level () (nth 0 (syntax-ppss)))
264 (defun zig-currently-in-str () (nth 3 (syntax-ppss)))
265 (defun zig-start-of-current-str-or-comment () (nth 8 (syntax-ppss)))
266
267 (defun zig-skip-backwards-past-whitespace-and-comments ()
268 (while (or
269 ;; If inside a comment, jump to start of comment.
270 (let ((start (zig-start-of-current-str-or-comment)))
271 (and start
272 (not (zig-currently-in-str))
273 (goto-char start)))
274 ;; Skip backwards past whitespace and comment end delimiters.
275 (/= 0 (skip-syntax-backward " >")))))
276
277 (defun zig-in-str-or-cmnt () (nth 8 (syntax-ppss)))
278 (defconst zig-top-item-beg-re
279 (concat "^ *"
280 (regexp-opt
281 '("pub" "extern" "export" ""))
282 "[[:space:]]*"
283 (regexp-opt
284 '("fn" "test"))
285 "[[:space:]]+")
286 "Start of a Zig item.")
287
288 (defun zig-beginning-of-defun (&optional arg)
289 "Move backward to the beginning of the current defun.
290
291 With ARG, move backward multiple defuns. Negative ARG means
292 move forward.
293
294 This is written mainly to be used as `beginning-of-defun-function' for Zig."
295 (interactive "p")
296 (let* ((arg (or arg 1))
297 (magnitude (abs arg))
298 (sign (if (< arg 0) -1 1)))
299 ;; If moving forward, don't find the defun we might currently be
300 ;; on.
301 (when (< sign 0)
302 (end-of-line))
303 (catch 'done
304 (dotimes (_ magnitude)
305 ;; Search until we find a match that is not in a string or comment.
306 (while (if (re-search-backward (concat "^[[:space:]]*\\(" zig-top-item-beg-re "\\)")
307 nil 'move sign)
308 (zig-in-str-or-cmnt)
309 ;; Did not find it.
310 (throw 'done nil)))))
311 t))
312
313 (defun zig-end-of-defun ()
314 "Move forward to the next end of defun.
315
316 With argument, do it that many times.
317 Negative argument -N means move back to Nth preceding end of defun.
318
319 Assume that this is called after `beginning-of-defun'. So point is
320 at the beginning of the defun body.
321
322 This is written mainly to be used as `end-of-defun-function' for Zig."
323 (interactive)
324
325 ;; Jump over the function parameters and paren-wrapped return, if they exist.
326 (while (re-search-forward "(" (line-end-position) t)
327 (progn
328 (backward-char)
329 (forward-sexp)))
330
331 ;; Find the opening brace
332 (if (re-search-forward "[{]" nil t)
333 (progn
334 (goto-char (match-beginning 0))
335 ;; Go to the closing brace
336 (condition-case nil
337 (forward-sexp)
338 (scan-error
339 (goto-char (point-max))))
340 (end-of-line))
341 ;; There is no opening brace, so consider the whole buffer to be one "defun"
342 (goto-char (point-max))))
343
344 (defun zig-mode-indent-line ()
345 (interactive)
346 ;; First, calculate the column that this line should be indented to.
347 (let ((indent-col
348 (save-excursion
349 (back-to-indentation)
350 (let* (;; paren-level: How many sets of parens (or other delimiters)
351 ;; we're within, except that if this line closes the
352 ;; innermost set(s) (e.g. the line is just "}"), then we
353 ;; don't count those set(s).
354 (paren-level
355 (save-excursion
356 (while (looking-at "[]})]") (forward-char))
357 (zig-paren-nesting-level)))
358 ;; prev-block-indent-col: If we're within delimiters, this is
359 ;; the column to which the start of that block is indented
360 ;; (if we're not, this is just zero).
361 (prev-block-indent-col
362 (if (<= paren-level 0) 0
363 (save-excursion
364 (while (>= (zig-paren-nesting-level) paren-level)
365 (backward-up-list)
366 (back-to-indentation))
367 (current-column))))
368 ;; base-indent-col: The column to which a complete expression
369 ;; on this line should be indented.
370 (base-indent-col
371 (if (<= paren-level 0)
372 prev-block-indent-col
373 (or (save-excursion
374 (backward-up-list)
375 (forward-char)
376 (and (not (looking-at " *\\(//[^\n]*\\)?\n"))
377 (current-column)))
378 (+ prev-block-indent-col zig-indent-offset))))
379 ;; is-expr-continutation: True if this line continues an
380 ;; expression from the previous line, false otherwise.
381 (is-expr-continutation
382 (and
383 (not (looking-at "[]});]\\|else"))
384 (save-excursion
385 (zig-skip-backwards-past-whitespace-and-comments)
386 (when (> (point) 1)
387 (backward-char)
388 (not (looking-at "[,;([{}]")))))))
389 ;; Now we can calculate indent-col:
390 (if is-expr-continutation
391 (+ base-indent-col zig-indent-offset)
392 base-indent-col)))))
393 ;; If point is within the indentation whitespace, move it to the end of the
394 ;; new indentation whitespace (which is what the indent-line-to function
395 ;; always does). Otherwise, we don't want point to move, so we use a
396 ;; save-excursion.
397 (if (<= (current-column) (current-indentation))
398 (indent-line-to indent-col)
399 (save-excursion (indent-line-to indent-col)))))
400
401 (defun zig-syntax-propertize-to-newline-if-in-multiline-str (end)
402 ;; First, we need to check if we're in a multiline string literal; if we're
403 ;; not, do nothing.
404 (when (zig-currently-in-str)
405 (let ((start (zig-start-of-current-str-or-comment)))
406 (when (save-excursion
407 (goto-char start)
408 (looking-at "\\\\\\\\"))
409 ;; At this point, we've determined that we're within a multiline string
410 ;; literal. Let `stop' be the position of the closing newline, or
411 ;; `end', whichever comes first.
412 (let ((stop (if (save-excursion
413 (goto-char start)
414 (re-search-forward "\n" end t))
415 (prog1 (match-end 0)
416 ;; We found the closing newline, so mark it as the
417 ;; end of this string literal.
418 (put-text-property (match-beginning 0)
419 (match-end 0)
420 'syntax-table
421 (string-to-syntax "|")))
422 end)))
423 ;; Zig multiline string literals don't support escapes, so mark all
424 ;; backslashes (up to `stop') as punctation instead of escapes.
425 (save-excursion
426 (goto-char (1+ start))
427 (while (re-search-forward "\\\\" stop t)
428 (put-text-property (match-beginning 0) (match-end 0)
429 'syntax-table (string-to-syntax "."))
430 (goto-char (match-end 0))))
431 ;; Move to the end of the string (or `end'), so that
432 ;; zig-syntax-propertize can pick up from there.
433 (goto-char stop))))))
434
435 (defun zig-syntax-propertize (start end)
436 (goto-char start)
437 (zig-syntax-propertize-to-newline-if-in-multiline-str end)
438 (funcall
439 (syntax-propertize-rules
440 ;; Multiline strings
441 ;; Do not match backslashes that are preceded by single or
442 ;; double-quotes.
443 ("[^\\'\"]c?\\(\\\\\\)\\\\"
444 (1 (prog1 "|"
445 (goto-char (match-end 0))
446 (zig-syntax-propertize-to-newline-if-in-multiline-str end)))))
447 (point) end))
448
449 (defun zig-mode-syntactic-face-function (state)
450 (if (nth 3 state)
451 (save-excursion
452 (goto-char (nth 8 state))
453 (if (looking-at "\\\\\\\\")
454 'zig-multiline-string-face
455 'font-lock-string-face))
456 (save-excursion
457 (goto-char (nth 8 state))
458 (if (looking-at "//[/|!][^/]")
459 'font-lock-doc-face
460 'font-lock-comment-face))))
461
462 ;;; Imenu support
463 (defun zig-re-structure-def-imenu (stype)
464 "Construct a regular expression for strucutres definitions of type STYPE."
465 (concat (zig-re-word "const") "[[:space:]]+"
466 (zig-re-grab zig-re-identifier)
467 ".*"
468 (zig-re-word stype)))
469
470 (defvar zig-imenu-generic-expression
471 (append (mapcar (lambda (x)
472 (list (capitalize x) (zig-re-structure-def-imenu x) 1))
473 '("enum" "struct" "union"))
474 `(("Fn" ,(zig-re-definition "fn") 1))))
475
476 ;;; Guarantee filesystem unix line endings
477 (defun zig-file-coding-system ()
478 (with-current-buffer (current-buffer)
479 (if (buffer-file-name)
480 (if (string-match "\\.d?zig\\'" buffer-file-name)
481 (setq buffer-file-coding-system 'utf-8-unix)
482 nil))
483 ))
484
485 (add-hook 'zig-mode-hook 'zig-file-coding-system)
486
487 (defvar zig-mode-map
488 (let ((map (make-sparse-keymap)))
489 (define-key map (kbd "C-c C-b") 'zig-compile)
490 (define-key map (kbd "C-c C-f") 'zig-format-buffer)
491 (define-key map (kbd "C-c C-r") 'zig-run)
492 (define-key map (kbd "C-c C-t") 'zig-test-buffer)
493 map)
494 "Keymap for Zig major mode.")
495
496 ;;;###autoload
497 (define-derived-mode zig-mode prog-mode "Zig"
498 "A major mode for the Zig programming language.
499
500 \\{zig-mode-map}"
501 :group 'zig-mode
502 (setq-local comment-start "// ")
503 (setq-local comment-start-skip "//+ *")
504 (setq-local comment-end "")
505 (setq-local electric-indent-chars
506 (append zig-electric-indent-chars
507 (and (boundp 'electric-indent-chars)
508 electric-indent-chars)))
509 (setq-local beginning-of-defun-function 'zig-beginning-of-defun)
510 (setq-local end-of-defun-function 'zig-end-of-defun)
511 (setq-local indent-line-function 'zig-mode-indent-line)
512 (setq-local indent-tabs-mode nil) ; Zig forbids tab characters.
513 (setq-local syntax-propertize-function 'zig-syntax-propertize)
514 (setq-local imenu-generic-expression zig-imenu-generic-expression)
515 (setq font-lock-defaults '(zig-font-lock-keywords
516 nil nil nil nil
517 (font-lock-syntactic-face-function . zig-mode-syntactic-face-function)))
518
519 (when zig-format-on-save
520 (zig-format-on-save-mode 1)))
521
522 ;;;###autoload
523 (add-to-list 'auto-mode-alist '("\\.\\(zig\\|zon\\)\\'" . zig-mode))
524
525 (provide 'zig-mode)
526 ;;; zig-mode.el ends here