diff --git a/zig-base-mode.el b/zig-base-mode.el new file mode 100644 index 0000000..a595757 --- /dev/null +++ b/zig-base-mode.el @@ -0,0 +1,417 @@ +;;; zig-base-mode.el --- A generic major mode for the Zig programming language -*- lexical-binding: t -*- + +;; Version: 0.0.8 +;; Author: Andrea Orru , Andrew Kelley +;; Keywords: zig, languages +;; Package-Requires: ((emacs "24.3") (reformatter "0.6")) +;; Homepage: https://github.com/zig-lang/zig-mode + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 . + +;;; Commentary: + +;; A generic major mode for the Zig programming languages. + +;; See documentation on https://github.com/zig-lang/zig-mode + +;;; Code: + +(require 'reformatter) + +(defgroup zig nil + "Support for Zig code." + :link '(url-link "https://ziglang.org/") + :prefix "zig-" + :group 'languages) + +(defcustom zig-indent-offset 4 + "Indent Zig code by this number of spaces." + :type 'integer + :group 'zig-mode + :safe #'integerp) + +(defcustom zig-format-on-save t + "Format buffers before saving using zig fmt." + :type 'boolean + :safe #'booleanp + :group 'zig-mode) + +(defcustom zig-format-show-buffer t + "Show a *zig-fmt* buffer after zig fmt completes with errors" + :type 'boolean + :safe #'booleanp + :group 'zig-mode) + +(defcustom zig-zig-bin "zig" + "Path to zig executable." + :type 'file + :safe #'stringp + :group 'zig-mode) + +(defcustom zig-run-optimization-mode "Debug" + "Optimization mode to run code with." + :type 'string + :safe #'stringp + :group 'zig-mode) + +(defcustom zig-test-optimization-mode "Debug" + "Optimization mode to run tests with." + :type 'string + :safe #'stringp + :group 'zig-mode) + +;; zig CLI commands + +(defun zig--run-cmd (cmd &optional source &rest args) + "Use compile command to execute a zig CMD with ARGS if given. +If given a SOURCE, execute the CMD on it." + (let ((cmd-args + (if source + (mapconcat 'shell-quote-argument (cons source args) " ") + args))) + (compilation-start (concat zig-zig-bin " " cmd " " cmd-args)))) + +;;;###autoload +(defun zig-compile () + "Compile using `zig build`." + (interactive) + (zig--run-cmd "build")) + +;;;###autoload +(defun zig-build-exe () + "Create executable from source or object file." + (interactive) + (zig--run-cmd "build-exe" (file-local-name (buffer-file-name)))) + +;;;###autoload +(defun zig-build-lib () + "Create library from source or assembly." + (interactive) + (zig--run-cmd "build-lib" (file-local-name (buffer-file-name)))) + +;;;###autoload +(defun zig-build-obj () + "Create object from source or assembly." + (interactive) + (zig--run-cmd "build-obj" (file-local-name (buffer-file-name)))) + +;;;###autoload +(defun zig-test-buffer () + "Test buffer using `zig test`." + (interactive) + (zig--run-cmd "test" (file-local-name (buffer-file-name)) "-O" zig-test-optimization-mode)) + +;;;###autoload +(defun zig-run () + "Create an executable from the current buffer and run it immediately." + (interactive) + (zig--run-cmd "run" (file-local-name (buffer-file-name)) "-O" zig-run-optimization-mode)) + +;; zig fmt + +(reformatter-define zig-format + :program zig-zig-bin + :args '("fmt" "--stdin") + :group 'zig-mode + :lighter " ZigFmt") + +;;;###autoload (autoload 'zig-format-buffer "current-file" nil t) +;;;###autoload (autoload 'zig-format-region "current-file" nil t) +;;;###autoload (autoload 'zig-format-on-save-mode "current-file" nil t) + +(defun zig-re-word (inner) + "Construct a regular expression for the word INNER." + (concat "\\<" inner "\\>")) + +(defun zig-re-grab (inner) + "Construct a group regular expression for INNER." + (concat "\\(" inner "\\)")) + +(defconst zig-re-optional "\\(?:[[:space:]]*\\?[[:space:]]*\\)") +(defconst zig-re-pointer "\\(?:[[:space:]]*\\*\\(?:const[[:space:]]*\\)?[[:space:]]*\\)") +(defconst zig-re-array "\\(?:[[:space:]]*\\[[^]]*\\]\\(?:const[[:space:]]*\\)?[[:space:]]*\\)") + +(defconst zig-re-optionals-pointers-arrays + (concat "\\(?:" zig-re-optional "\\|" zig-re-pointer "\\|" zig-re-array "\\)*")) + +(defconst zig-re-identifier "[[:word:]_][[:word:]_[:digit:]]*") +(defconst zig-re-type-annotation + (concat (zig-re-grab zig-re-identifier) + "[[:space:]]*:[[:space:]]*" + zig-re-optionals-pointers-arrays + (zig-re-grab zig-re-identifier))) + +(defun zig-re-definition (dtype) + "Construct a regular expression for definitions of type DTYPE." + (concat (zig-re-word dtype) "[[:space:]]+" (zig-re-grab zig-re-identifier))) + +(defconst zig-mode-syntax-table + (let ((table (make-syntax-table))) + + ;; Operators + (dolist (i '(?+ ?- ?* ?/ ?% ?& ?| ?= ?! ?< ?>)) + (modify-syntax-entry i "." table)) + + ;; Strings + (modify-syntax-entry ?\' "\"" table) + (modify-syntax-entry ?\" "\"" table) + (modify-syntax-entry ?\\ "\\" table) + + ;; Comments + (modify-syntax-entry ?/ ". 12" table) + (modify-syntax-entry ?\n ">" table) + + table)) + +(defconst zig-keywords + '( + ;; Storage + "const" "var" "extern" "packed" "export" "pub" "noalias" "inline" + "noinline" "comptime" "callconv" "volatile" "allowzero" + "align" "linksection" "threadlocal" "addrspace" + + ;; Structure + "struct" "enum" "union" "error" "opaque" + + ;; Statement + "break" "return" "continue" "asm" "defer" "errdefer" "unreachable" + "try" "catch" "async" "nosuspend" "await" "suspend" "resume" + + ;; Conditional + "if" "else" "switch" "and" "or" "orelse" + + ;; Repeat + "while" "for" + + ;; Other keywords + "fn" "usingnamespace" "test")) + +(defconst zig-types + '( + ;; Integer types + "i2" "u2" "i3" "u3" "i4" "u4" "i5" "u5" "i6" "u6" "i7" "u7" "i8" "u8" + "i16" "u16" "i29" "u29" "i32" "u32" "i64" "u64" "i128" "u128" + "isize" "usize" + + ;; Floating types + "f16" "f32" "f64" "f80" "f128" + + ;; C types + "c_char" "c_short" "c_ushort" "c_int" "c_uint" "c_long" "c_ulong" + "c_longlong" "c_ulonglong" "c_longdouble" + + ;; Comptime types + "comptime_int" "comptime_float" + + ;; Other types + "bool" "void" "noreturn" "type" "error" "anyerror" "anyframe" "anytype" + "anyopaque")) + +(defconst zig-constants + '( + ;; Boolean + "true" "false" + + ;; Other constants + "null" "undefined")) + +(defconst zig-electric-indent-chars + '(?\; ?, ?\) ?\] ?})) + +(defface zig-multiline-string-face + '((t :inherit font-lock-string-face)) + "Face for multiline string literals." + :group 'zig-mode) + +(defun zig-paren-nesting-level () (nth 0 (syntax-ppss))) +(defun zig-currently-in-str () (nth 3 (syntax-ppss))) +(defun zig-start-of-current-str-or-comment () (nth 8 (syntax-ppss))) + +(defun zig-skip-backwards-past-whitespace-and-comments () + (while (or + ;; If inside a comment, jump to start of comment. + (let ((start (zig-start-of-current-str-or-comment))) + (and start + (not (zig-currently-in-str)) + (goto-char start))) + ;; Skip backwards past whitespace and comment end delimiters. + (/= 0 (skip-syntax-backward " >"))))) + +(defun zig-in-str-or-cmnt () (nth 8 (syntax-ppss))) +(defconst zig-top-item-beg-re + (concat "^ *" + (regexp-opt + '("pub" "extern" "export" "")) + "[[:space:]]*" + (regexp-opt + '("fn" "test")) + "[[:space:]]+") + "Start of a Zig item.") + +(defun zig-beginning-of-defun (&optional arg) + "Move backward to the beginning of the current defun. + +With ARG, move backward multiple defuns. Negative ARG means +move forward. + +This is written mainly to be used as `beginning-of-defun-function' for Zig." + (interactive "p") + (let* ((arg (or arg 1)) + (magnitude (abs arg)) + (sign (if (< arg 0) -1 1))) + ;; If moving forward, don't find the defun we might currently be + ;; on. + (when (< sign 0) + (end-of-line)) + (catch 'done + (dotimes (_ magnitude) + ;; Search until we find a match that is not in a string or comment. + (while (if (re-search-backward (concat "^[[:space:]]*\\(" zig-top-item-beg-re "\\)") + nil 'move sign) + (zig-in-str-or-cmnt) + ;; Did not find it. + (throw 'done nil))))) + t)) + +(defun zig-end-of-defun () + "Move forward to the next end of defun. + +With argument, do it that many times. +Negative argument -N means move back to Nth preceding end of defun. + +Assume that this is called after `beginning-of-defun'. So point is +at the beginning of the defun body. + +This is written mainly to be used as `end-of-defun-function' for Zig." + (interactive) + + ;; Jump over the function parameters and paren-wrapped return, if they exist. + (while (re-search-forward "(" (line-end-position) t) + (progn + (backward-char) + (forward-sexp))) + + ;; Find the opening brace + (if (re-search-forward "[{]" nil t) + (progn + (goto-char (match-beginning 0)) + ;; Go to the closing brace + (condition-case nil + (forward-sexp) + (scan-error + (goto-char (point-max)))) + (end-of-line)) + ;; There is no opening brace, so consider the whole buffer to be one "defun" + (goto-char (point-max)))) + +(defun zig-mode-indent-line () + (interactive) + ;; First, calculate the column that this line should be indented to. + (let ((indent-col + (save-excursion + (back-to-indentation) + (let* (;; paren-level: How many sets of parens (or other delimiters) + ;; we're within, except that if ppthis line closes the + ;; innermost set(s) (e.g. the line is just "}"), then we + ;; don't count those set(s). + (paren-level + (save-excursion + (while (looking-at "[]})]") (forward-char)) + (zig-paren-nesting-level))) + ;; prev-block-indent-col: If we're within delimiters, this is + ;; the column to which the start of that block is indented + ;; (if we're not, this is just zero). + (prev-block-indent-col + (if (<= paren-level 0) 0 + (save-excursion + (while (>= (zig-paren-nesting-level) paren-level) + (backward-up-list) + (back-to-indentation)) + (current-column)))) + ;; base-indent-col: The column to which a complete expression + ;; on this line should be indented. + (base-indent-col + (if (<= paren-level 0) + prev-block-indent-col + (or (save-excursion + (backward-up-list) + (forward-char) + (and (not (looking-at " *\\(//[^\n]*\\)?\n")) + (current-column))) + (+ prev-block-indent-col zig-indent-offset)))) + ;; is-expr-continutation: True if this line continues an + ;; expression from the previous line, false otherwise. + (is-expr-continutation + (and + (not (looking-at "[]});]\\|else")) + (save-excursion + (zig-skip-backwards-past-whitespace-and-comments) + (when (> (point) 1) + (backward-char) + (not (looking-at "[,;([{}]"))))))) + ;; Now we can calculate indent-col: + (if is-expr-continutation + (+ base-indent-col zig-indent-offset) + base-indent-col))))) + ;; If point is within the indentation whitespace, move it to the end of the + ;; new indentation whitespace (which is what the indent-line-to function + ;; always does). Otherwise, we don't want point to move, so we use a + ;; save-excursion. + (if (<= (current-column) (current-indentation)) + (indent-line-to indent-col) + (save-excursion (indent-line-to indent-col))))) + +;;; Imenu support +(defun zig-re-structure-def-imenu (stype) + "Construct a regular expression for strucutres definitions of type STYPE." + (concat (zig-re-word "const") "[[:space:]]+" + (zig-re-grab zig-re-identifier) + ".*" + (zig-re-word stype))) + +(defvar zig-imenu-generic-expression + (append (mapcar (lambda (x) + (list (capitalize x) (zig-re-structure-def-imenu x) 1)) + '("enum" "struct" "union")) + `(("Fn" ,(zig-re-definition "fn") 1)))) + +;;; Guarantee filesystem unix line endings +(defun zig-file-coding-system () + (with-current-buffer (current-buffer) + (if (buffer-file-name) + (if (string-match "\\.d?zig\\'" buffer-file-name) + (setq buffer-file-coding-system 'utf-8-unix) + nil)) +)) + +;;;###autoload +(define-derived-mode zig-base-mode prog-mode "Zig" + "A generic major mode for the Zig programming language. + +This mode is intended to be derived by concrete major modes." + (setq-local comment-start "// ") + (setq-local comment-start-skip "//+ *") + (setq-local comment-end "") + (setq-local electric-indent-chars + (append zig-electric-indent-chars + (and (boundp 'electric-indent-chars) + electric-indent-chars))) + (setq-local indent-tabs-mode nil) ; Zig forbids tab characters. + + (when zig-format-on-save + (zig-format-on-save-mode 1))) + +(provide 'zig-base-mode) + +;;; zig-base-mode.el ends here diff --git a/zig-mode.el b/zig-mode.el index 447d4aa..35fdd7e 100644 --- a/zig-mode.el +++ b/zig-mode.el @@ -21,218 +21,13 @@ ;;; Commentary: -;; A major mode for the Zig programming languages. +;; A concrete implementation of a major mode for the Zig programming languages dervied from zig-base-mode. ;; See documentation on https://github.com/zig-lang/zig-mode ;;; Code: -(require 'reformatter) - -(defgroup zig-mode nil - "Support for Zig code." - :link '(url-link "https://ziglang.org/") - :group 'languages) - -(defcustom zig-indent-offset 4 - "Indent Zig code by this number of spaces." - :type 'integer - :group 'zig-mode - :safe #'integerp) - -(defcustom zig-format-on-save t - "Format buffers before saving using zig fmt." - :type 'boolean - :safe #'booleanp - :group 'zig-mode) - -(defcustom zig-format-show-buffer t - "Show a *zig-fmt* buffer after zig fmt completes with errors" - :type 'boolean - :safe #'booleanp - :group 'zig-mode) - -(defcustom zig-zig-bin "zig" - "Path to zig executable." - :type 'file - :safe #'stringp - :group 'zig-mode) - -(defcustom zig-run-optimization-mode "Debug" - "Optimization mode to run code with." - :type 'string - :safe #'stringp - :group 'zig-mode) - -(defcustom zig-test-optimization-mode "Debug" - "Optimization mode to run tests with." - :type 'string - :safe #'stringp - :group 'zig-mode) - -;; zig CLI commands - -(defun zig--run-cmd (cmd &optional source &rest args) - "Use compile command to execute a zig CMD with ARGS if given. -If given a SOURCE, execute the CMD on it." - (let ((cmd-args - (if source - (mapconcat 'shell-quote-argument (cons source args) " ") - args))) - (compilation-start (concat zig-zig-bin " " cmd " " cmd-args)))) - -;;;###autoload -(defun zig-compile () - "Compile using `zig build`." - (interactive) - (zig--run-cmd "build")) - -;;;###autoload -(defun zig-build-exe () - "Create executable from source or object file." - (interactive) - (zig--run-cmd "build-exe" (file-local-name (buffer-file-name)))) - -;;;###autoload -(defun zig-build-lib () - "Create library from source or assembly." - (interactive) - (zig--run-cmd "build-lib" (file-local-name (buffer-file-name)))) - -;;;###autoload -(defun zig-build-obj () - "Create object from source or assembly." - (interactive) - (zig--run-cmd "build-obj" (file-local-name (buffer-file-name)))) - -;;;###autoload -(defun zig-test-buffer () - "Test buffer using `zig test`." - (interactive) - (zig--run-cmd "test" (file-local-name (buffer-file-name)) "-O" zig-test-optimization-mode)) - -;;;###autoload -(defun zig-run () - "Create an executable from the current buffer and run it immediately." - (interactive) - (zig--run-cmd "run" (file-local-name (buffer-file-name)) "-O" zig-run-optimization-mode)) - -;; zig fmt - -(reformatter-define zig-format - :program zig-zig-bin - :args '("fmt" "--stdin") - :group 'zig-mode - :lighter " ZigFmt") - -;;;###autoload (autoload 'zig-format-buffer "current-file" nil t) -;;;###autoload (autoload 'zig-format-region "current-file" nil t) -;;;###autoload (autoload 'zig-format-on-save-mode "current-file" nil t) - -(defun zig-re-word (inner) - "Construct a regular expression for the word INNER." - (concat "\\<" inner "\\>")) - -(defun zig-re-grab (inner) - "Construct a group regular expression for INNER." - (concat "\\(" inner "\\)")) - -(defconst zig-re-optional "\\(?:[[:space:]]*\\?[[:space:]]*\\)") -(defconst zig-re-pointer "\\(?:[[:space:]]*\\*\\(?:const[[:space:]]*\\)?[[:space:]]*\\)") -(defconst zig-re-array "\\(?:[[:space:]]*\\[[^]]*\\]\\(?:const[[:space:]]*\\)?[[:space:]]*\\)") - -(defconst zig-re-optionals-pointers-arrays - (concat "\\(?:" zig-re-optional "\\|" zig-re-pointer "\\|" zig-re-array "\\)*")) - -(defconst zig-re-identifier "[[:word:]_][[:word:]_[:digit:]]*") -(defconst zig-re-type-annotation - (concat (zig-re-grab zig-re-identifier) - "[[:space:]]*:[[:space:]]*" - zig-re-optionals-pointers-arrays - (zig-re-grab zig-re-identifier))) - -(defun zig-re-definition (dtype) - "Construct a regular expression for definitions of type DTYPE." - (concat (zig-re-word dtype) "[[:space:]]+" (zig-re-grab zig-re-identifier))) - -(defconst zig-mode-syntax-table - (let ((table (make-syntax-table))) - - ;; Operators - (dolist (i '(?+ ?- ?* ?/ ?% ?& ?| ?= ?! ?< ?>)) - (modify-syntax-entry i "." table)) - - ;; Strings - (modify-syntax-entry ?\' "\"" table) - (modify-syntax-entry ?\" "\"" table) - (modify-syntax-entry ?\\ "\\" table) - - ;; Comments - (modify-syntax-entry ?/ ". 12" table) - (modify-syntax-entry ?\n ">" table) - - table)) - -(defconst zig-keywords - '( - ;; Storage - "const" "var" "extern" "packed" "export" "pub" "noalias" "inline" - "noinline" "comptime" "callconv" "volatile" "allowzero" - "align" "linksection" "threadlocal" "addrspace" - - ;; Structure - "struct" "enum" "union" "error" "opaque" - - ;; Statement - "break" "return" "continue" "asm" "defer" "errdefer" "unreachable" - "try" "catch" "async" "nosuspend" "await" "suspend" "resume" - - ;; Conditional - "if" "else" "switch" "and" "or" "orelse" - - ;; Repeat - "while" "for" - - ;; Other keywords - "fn" "usingnamespace" "test")) - -(defconst zig-types - '( - ;; Integer types - "i2" "u2" "i3" "u3" "i4" "u4" "i5" "u5" "i6" "u6" "i7" "u7" "i8" "u8" - "i16" "u16" "i29" "u29" "i32" "u32" "i64" "u64" "i128" "u128" - "isize" "usize" - - ;; Floating types - "f16" "f32" "f64" "f80" "f128" - - ;; C types - "c_char" "c_short" "c_ushort" "c_int" "c_uint" "c_long" "c_ulong" - "c_longlong" "c_ulonglong" "c_longdouble" - - ;; Comptime types - "comptime_int" "comptime_float" - - ;; Other types - "bool" "void" "noreturn" "type" "error" "anyerror" "anyframe" "anytype" - "anyopaque")) - -(defconst zig-constants - '( - ;; Boolean - "true" "false" - - ;; Other constants - "null" "undefined")) - -(defconst zig-electric-indent-chars - '(?\; ?, ?\) ?\] ?})) - - -(defface zig-multiline-string-face - '((t :inherit font-lock-string-face)) - "Face for multiline string literals." - :group 'zig-mode) +(require 'zig-base-mode) (defvar zig-font-lock-keywords (append @@ -258,143 +53,6 @@ If given a SOURCE, execute the CMD on it." ("var" . font-lock-variable-name-face) ("fn" . font-lock-function-name-face))))) -(defun zig-paren-nesting-level () (nth 0 (syntax-ppss))) -(defun zig-currently-in-str () (nth 3 (syntax-ppss))) -(defun zig-start-of-current-str-or-comment () (nth 8 (syntax-ppss))) - -(defun zig-skip-backwards-past-whitespace-and-comments () - (while (or - ;; If inside a comment, jump to start of comment. - (let ((start (zig-start-of-current-str-or-comment))) - (and start - (not (zig-currently-in-str)) - (goto-char start))) - ;; Skip backwards past whitespace and comment end delimiters. - (/= 0 (skip-syntax-backward " >"))))) - -(defun zig-in-str-or-cmnt () (nth 8 (syntax-ppss))) -(defconst zig-top-item-beg-re - (concat "^ *" - (regexp-opt - '("pub" "extern" "export" "")) - "[[:space:]]*" - (regexp-opt - '("fn" "test")) - "[[:space:]]+") - "Start of a Zig item.") - -(defun zig-beginning-of-defun (&optional arg) - "Move backward to the beginning of the current defun. - -With ARG, move backward multiple defuns. Negative ARG means -move forward. - -This is written mainly to be used as `beginning-of-defun-function' for Zig." - (interactive "p") - (let* ((arg (or arg 1)) - (magnitude (abs arg)) - (sign (if (< arg 0) -1 1))) - ;; If moving forward, don't find the defun we might currently be - ;; on. - (when (< sign 0) - (end-of-line)) - (catch 'done - (dotimes (_ magnitude) - ;; Search until we find a match that is not in a string or comment. - (while (if (re-search-backward (concat "^[[:space:]]*\\(" zig-top-item-beg-re "\\)") - nil 'move sign) - (zig-in-str-or-cmnt) - ;; Did not find it. - (throw 'done nil))))) - t)) - -(defun zig-end-of-defun () - "Move forward to the next end of defun. - -With argument, do it that many times. -Negative argument -N means move back to Nth preceding end of defun. - -Assume that this is called after `beginning-of-defun'. So point is -at the beginning of the defun body. - -This is written mainly to be used as `end-of-defun-function' for Zig." - (interactive) - - ;; Jump over the function parameters and paren-wrapped return, if they exist. - (while (re-search-forward "(" (line-end-position) t) - (progn - (backward-char) - (forward-sexp))) - - ;; Find the opening brace - (if (re-search-forward "[{]" nil t) - (progn - (goto-char (match-beginning 0)) - ;; Go to the closing brace - (condition-case nil - (forward-sexp) - (scan-error - (goto-char (point-max)))) - (end-of-line)) - ;; There is no opening brace, so consider the whole buffer to be one "defun" - (goto-char (point-max)))) - -(defun zig-mode-indent-line () - (interactive) - ;; First, calculate the column that this line should be indented to. - (let ((indent-col - (save-excursion - (back-to-indentation) - (let* (;; paren-level: How many sets of parens (or other delimiters) - ;; we're within, except that if this line closes the - ;; innermost set(s) (e.g. the line is just "}"), then we - ;; don't count those set(s). - (paren-level - (save-excursion - (while (looking-at "[]})]") (forward-char)) - (zig-paren-nesting-level))) - ;; prev-block-indent-col: If we're within delimiters, this is - ;; the column to which the start of that block is indented - ;; (if we're not, this is just zero). - (prev-block-indent-col - (if (<= paren-level 0) 0 - (save-excursion - (while (>= (zig-paren-nesting-level) paren-level) - (backward-up-list) - (back-to-indentation)) - (current-column)))) - ;; base-indent-col: The column to which a complete expression - ;; on this line should be indented. - (base-indent-col - (if (<= paren-level 0) - prev-block-indent-col - (or (save-excursion - (backward-up-list) - (forward-char) - (and (not (looking-at " *\\(//[^\n]*\\)?\n")) - (current-column))) - (+ prev-block-indent-col zig-indent-offset)))) - ;; is-expr-continutation: True if this line continues an - ;; expression from the previous line, false otherwise. - (is-expr-continutation - (and - (not (looking-at "[]});]\\|else")) - (save-excursion - (zig-skip-backwards-past-whitespace-and-comments) - (when (> (point) 1) - (backward-char) - (not (looking-at "[,;([{}]"))))))) - ;; Now we can calculate indent-col: - (if is-expr-continutation - (+ base-indent-col zig-indent-offset) - base-indent-col))))) - ;; If point is within the indentation whitespace, move it to the end of the - ;; new indentation whitespace (which is what the indent-line-to function - ;; always does). Otherwise, we don't want point to move, so we use a - ;; save-excursion. - (if (<= (current-column) (current-indentation)) - (indent-line-to indent-col) - (save-excursion (indent-line-to indent-col))))) (defun zig-syntax-propertize-to-newline-if-in-multiline-str (end) ;; First, we need to check if we're in a multiline string literal; if we're @@ -457,65 +115,32 @@ This is written mainly to be used as `end-of-defun-function' for Zig." 'font-lock-doc-face 'font-lock-comment-face)))) -;;; Imenu support -(defun zig-re-structure-def-imenu (stype) - "Construct a regular expression for strucutres definitions of type STYPE." - (concat (zig-re-word "const") "[[:space:]]+" - (zig-re-grab zig-re-identifier) - ".*" - (zig-re-word stype))) - -(defvar zig-imenu-generic-expression - (append (mapcar (lambda (x) - (list (capitalize x) (zig-re-structure-def-imenu x) 1)) - '("enum" "struct" "union")) - `(("Fn" ,(zig-re-definition "fn") 1)))) - -;;; Guarantee filesystem unix line endings -(defun zig-file-coding-system () - (with-current-buffer (current-buffer) - (if (buffer-file-name) - (if (string-match "\\.d?zig\\'" buffer-file-name) - (setq buffer-file-coding-system 'utf-8-unix) - nil)) -)) - -(add-hook 'zig-mode-hook 'zig-file-coding-system) - (defvar zig-mode-map (let ((map (make-sparse-keymap))) - (define-key map (kbd "C-c C-b") 'zig-compile) - (define-key map (kbd "C-c C-f") 'zig-format-buffer) - (define-key map (kbd "C-c C-r") 'zig-run) - (define-key map (kbd "C-c C-t") 'zig-test-buffer) - map) + (define-key map (kbd "C-c C-b") 'zig-compile) + (define-key map (kbd "C-c C-f") 'zig-format-buffer) + (define-key map (kbd "C-c C-r") 'zig-run) + (define-key map (kbd "C-c C-t") 'zig-test-buffer) + map) "Keymap for Zig major mode.") +(add-hook 'zig-mode-hook 'zig-file-coding-system) + ;;;###autoload -(define-derived-mode zig-mode prog-mode "Zig" +(define-derived-mode zig-mode zig-base-mode "Zig" "A major mode for the Zig programming language. \\{zig-mode-map}" :group 'zig-mode - (setq-local comment-start "// ") - (setq-local comment-start-skip "//+ *") - (setq-local comment-end "") - (setq-local electric-indent-chars - (append zig-electric-indent-chars - (and (boundp 'electric-indent-chars) - electric-indent-chars))) + (setq font-lock-defaults '(zig-font-lock-keywords + nil nil nil nil + (font-lock-syntactic-face-function . zig-mode-syntactic-face-function))) (setq-local beginning-of-defun-function 'zig-beginning-of-defun) (setq-local end-of-defun-function 'zig-end-of-defun) (setq-local indent-line-function 'zig-mode-indent-line) - (setq-local indent-tabs-mode nil) ; Zig forbids tab characters. (setq-local syntax-propertize-function 'zig-syntax-propertize) - (setq-local imenu-generic-expression zig-imenu-generic-expression) - (setq font-lock-defaults '(zig-font-lock-keywords - nil nil nil nil - (font-lock-syntactic-face-function . zig-mode-syntactic-face-function))) + (setq-local imenu-generic-expression zig-imenu-generic-expression)) - (when zig-format-on-save - (zig-format-on-save-mode 1))) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.\\(zig\\|zon\\)\\'" . zig-mode)) diff --git a/zig-ts-mode.el b/zig-ts-mode.el new file mode 100644 index 0000000..34cf04d --- /dev/null +++ b/zig-ts-mode.el @@ -0,0 +1,265 @@ +;;; zig-ts-mode.el --- A tree-sitter enabled major mode for the Zig programming language -*- lexical-binding: t -*- + +;; Version: 0.0.8 +;; Author: Nan Zhong +;; Keywords: zig, languages, tree-sitter +;; Package-Requires: ((emacs "29.1") (reformatter "0.6")) +;; Homepage: https://github.com/zig-lang/zig-mode + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 . + +;;; Commentary: + +;; A tree-sitter enabled major mode for the Zig programming languages making use +;; of the tree-sitter grammar from https://github.com/maxxnino/tree-sitter-zig. +;; Syntax highlighting and indentation queries are adapted from that source. + +;; See documentation on https://github.com/zig-lang/zig-mode + +;;; Code: + +(require 'zig-base-mode) +(require 'treesit) +(eval-when-compile (require 'rx)) + +(defvar zig-ts-font-lock-rules + `(:language zig + :override t + :feature comment + (;; doc + [(container_doc_comment) + (doc_comment)] @font-lock-comment-face + + ;; line + (line_comment) @font-lock-comment-face) + + :language zig + :override t + :feature type + (;; TitleCase + ([variable_type_function: (IDENTIFIER) + field_access: (IDENTIFIER) + parameter: (IDENTIFIER)] @type + ;; "^[A-Z]([a-z]+[A-Za-z0-9]*)+$" + (:match ,(rx string-start + (char "A-Z") + (1+ (1+ (char "a-z")) + (0+ (char "A-Za-z0-9"))) + string-end) + @type)) @font-lock-type-face) + + :language zig + :override t + :feature constant + (field_constant: (IDENTIFIER) @font-lock-constant-face + ;; ALL_CAPS + ([variable_type_function: (IDENTIFIER) + field_access: (IDENTIFIER)] @constant + ;; "^[A-Z][A-Z_0-9]+$" + (:match ,(rx string-start + (char "A-Z") + (1+ (char "A-Z_0-9")) + string-end) + @constant)) @font-lock-constant-face + + [,@zig-constants] @font-lock-constant-face) + + :language zig + :override t + :feature builtin + ((BuildinTypeExpr) @font-lock-builtin-face + (BUILTINIDENTIFIER) @font-lock-builtin-face + + ;; _ + ((IDENTIFIER) @builtin + (:equal @builtin "_")) @font-lock-builtin-face + + ;; C Pointers [*c]T + (PtrTypeStart "c") @font-lock-builtin-face) + + :language zig + :override t + :feature variable + ([variable: (IDENTIFIER) + variable_type_function: (IDENTIFIER)] @font-lock-variable-name-face + + parameter: (IDENTIFIER) @font-lock-variable-name-face + + [field_member: (IDENTIFIER) + field_access: (IDENTIFIER)] @font-lock-property-name-face) + + :language zig + :override t + :feature function + ([function_call: (IDENTIFIER) + function: (IDENTIFIER)] @font-lock-function-name-face + + ;; camelCase + ([variable_type_function: (IDENTIFIER) + field_access: (IDENTIFIER) + parameter: (IDENTIFIER)] @function + ;; "^[a-z]+([A-Z][a-z0-9]*)+$" + (:match ,(rx string-start + (1+ (char "a-z")) + (1+ (char "A-Z") + (0+ (char "a-z0-9"))) + string-end) + @function)) + @font-lock-function-name-face) + + :language zig + :override t + :feature keyword + (((IDENTIFIER) @builtin + (:equal @builtin "_")) @font-lock-builtin-face + + ((BUILTINIDENTIFIER) @keyword + (:equal @keyword "@import")) @font-lock-keyword-face + + ((BUILTINIDENTIFIER) @keyword + (:equal @keyword "@cImport")) @font-lock-keyword-face + + exception: "!" @font-lock-keyword-face + (ERROR) @font-lock-keyword-face + + [,@zig-keywords] @font-lock-keyword-face) + + :language zig + :override t + :feature numeric + ([(INTEGER) + (FLOAT)] @font-lock-number-face) + + :language zig + :override t + :feature string + ([(LINESTRING) + (STRINGLITERALSINGLE)] @font-lock-string-face + + (CHAR_LITERAL) @font-lock-string-face + + (EscapeSequence) @font-lock-escape-face + + (FormatSequence) @font-lock-string-face) + + :language zig + :override t + :feature label + ((BreakLabel (IDENTIFIER)) @font-lock-type-face + (BlockLabel (IDENTIFIER)) @font-lock-type-face) + + :language zig + :feature operator + ([(CompareOp) + (BitwiseOp) + (BitShiftOp) + (AdditionOp) + (AssignOp) + (MultiplyOp) + (PrefixOp) + "*" + "**" + "->" + ".?" + ".*" + "?"] @font-lock-operator-face) + + :language zig + :feature punctuation + (["[" + "]" + "(" + ")" + "{" + "}" + (Payload "|") + (PtrPayload "|") + (PtrIndexPayload "|")] @font-lock-bracket-face + + [";" + "." + "," + ":"] @font-lock-delimiter-face + + [".." + "..."] @font-lock-punctuation-face))) + +(defvar zig-ts-indent-rules + `((zig + ((node-is ")") parent-bol 0) + ((node-is "]") parent-bol 0) + ((node-is "}") parent-bol 0) + ((parent-is "block") parent-bol zig-indent-offset) + ((parent-is "AsmExpr") parent-bol zig-indent-offset) + ((parent-is "AssignExpr") parent-bol zig-indent-offset) + ((parent-is "Block") parent-bol zig-indent-offset) + ((parent-is "BlockExpr") parent-bol zig-indent-offset) + ((parent-is "ContainerDecl") parent-bol zig-indent-offset) + ((parent-is "ErrorUnionExpr") parent-bol zig-indent-offset) + ((parent-is "InitList") parent-bol zig-indent-offset) + ((parent-is "SwitchExpr") parent-bol zig-indent-offset) + ((parent-is "TestDecl") parent-bol zig-indent-offset)))) + +(defun zig-ts-setup () + "Setup treesit for zig-ts-mode." + (interactive) + + ;; Font-lock + (setq-local treesit-font-lock-settings + (apply #'treesit-font-lock-rules + zig-ts-font-lock-rules)) + (setq-local treesit-font-lock-feature-list + '((comment) + (builtin keyword type) + (constant string numeric) + (variable function label operator punctuation))) + + ;; Indentation + (setq-local treesit-simple-indent-rules zig-ts-indent-rules) + + ;; TODO Navigation + ;; (setq-local treesit-defun-type-regexp ...) + ;; (setq-local treesit-defun-name-function ...) + + ;; TODO Imenu + ;; (setq-local treesit-simple-imenu-settings + + (treesit-major-mode-setup)) + +(defvar zig-ts-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-b") 'zig-compile) + (define-key map (kbd "C-c C-f") 'zig-format-buffer) + (define-key map (kbd "C-c C-r") 'zig-run) + (define-key map (kbd "C-c C-t") 'zig-test-buffer) + map) + "Keymap for Zig major mode.") + +;;;###autoload +(define-derived-mode zig-ts-mode zig-base-mode "Zig" + "A treesitter enabled major mode for the Zig programming language. + +\\{zig-ts-mode-map}" + :group 'zig-ts-mode + (setq-local font-lock-defaults nil) + (when (treesit-ready-p 'zig) + (treesit-parser-create 'zig) + (zig-ts-setup))) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.\\(zig\\|zon\\)\\'" . zig-ts-mode)) + +(provide 'zig-ts-mode) + +;;; zig-ts-mode.el ends here