aboutsummaryrefslogtreecommitdiff
path: root/.config/emacs/lisp/jai-mode.el
diff options
context:
space:
mode:
Diffstat (limited to '.config/emacs/lisp/jai-mode.el')
-rw-r--r--.config/emacs/lisp/jai-mode.el331
1 files changed, 331 insertions, 0 deletions
diff --git a/.config/emacs/lisp/jai-mode.el b/.config/emacs/lisp/jai-mode.el
new file mode 100644
index 0000000..6d42673
--- /dev/null
+++ b/.config/emacs/lisp/jai-mode.el
@@ -0,0 +1,331 @@
+;;; jai-mode.el --- Major mode for JAI -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2015-2023 Kristoffer Grönlund
+;; Copyright (C) 2023-2026 Valentin Ignatev
+
+;; Initial author: Kristoffer Grönlund <k@ziran.se>
+;; Current maintainer: Valentin Ignatev <mail@valigo.gg>
+;; URL: https://github.com/valignatev/jai-mode
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "26.1"))
+;; Keywords: languages
+
+;; This file is not part of GNU Emacs.
+
+;; This program 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 of the License, or
+;; (at your option) any later version.
+
+;; This program 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Major mode for JAI
+;;
+
+;;; Code:
+
+(require 'rx)
+(require 'js)
+(require 'compile)
+
+(defconst jai-mode-syntax-table
+ (let ((table (make-syntax-table)))
+ (modify-syntax-entry ?\" "\"" table)
+ (modify-syntax-entry ?\\ "\\" table)
+
+ ;; additional symbols
+ (modify-syntax-entry ?_ "w" table)
+
+ (modify-syntax-entry ?' "." table)
+ (modify-syntax-entry ?: "." table)
+ (modify-syntax-entry ?+ "." table)
+ (modify-syntax-entry ?- "." table)
+ (modify-syntax-entry ?% "." table)
+ (modify-syntax-entry ?& "." table)
+ (modify-syntax-entry ?| "." table)
+ (modify-syntax-entry ?^ "." table)
+ (modify-syntax-entry ?! "." table)
+ (modify-syntax-entry ?= "." table)
+ (modify-syntax-entry ?< "." table)
+ (modify-syntax-entry ?> "." table)
+ (modify-syntax-entry ?? "." table)
+
+ ;; Modify some syntax entries to allow nested block comments
+ (modify-syntax-entry ?/ ". 124b" table)
+ (modify-syntax-entry ?* ". 23n" table)
+ (modify-syntax-entry ?\n "> b" table)
+ (modify-syntax-entry ?\^m "> b" table)
+
+ table))
+
+
+;; This is needed for correct directive indentation.
+;; This jai-mode uses js-mode indentation, and it defines a bunch of C++-compatible crap
+;; for whatever reason. You could ask "what does javascript mode have to do with C++ preprocessor?",
+;; or "why does jai-mode use javascript indentation?".
+;; And my answer to that is - senator, I don't know, I just work here.
+;; This hack essentially turns those into "impossible" regexes
+;; (a word boundary at the beginning (`\\_<`) immediately followed by a word boundary at the end (`\\_>`))
+;; bypassing that codepath in js-mode entirely.
+(defconst cpp-font-lock-keywords-source-directives "\\_<\\_>")
+(defconst js--opt-cpp-start "\\_<\\_>")
+(defconst js--macro-decl-re "\\_<\\_>")
+
+(defconst jai-builtins
+ '("it" "it_index"))
+
+(defconst jai-keywords
+ '("if" "ifx" "else" "then" "while" "for" "switch" "case" "struct" "enum"
+ "return" "remove" "continue" "break" "defer" "inline" "no_inline"
+ "using" "code_of" "initializer_of" "size_of" "type_of" "cast" "type_info"
+ "null" "true" "false" "xx" "context" "operator" "push_context" "is_constant"
+ "enum_flags" "union" "interface"))
+
+(defconst jai-typenames
+ '("int" "u64" "u32" "u16" "u8"
+ "s64" "s32" "s16" "s8" "float"
+ "float32" "float64" "string"
+ "bool" "void"))
+
+(defun jai-wrap-word-rx (s)
+ (concat "\\<" s "\\>"))
+
+(defun jai-keywords-rx (keywords)
+ "build keyword regexp"
+ (jai-wrap-word-rx (regexp-opt keywords t)))
+
+(defconst jai-dollar-type-rx (rx (group "$" (or (1+ word) (opt "$")))))
+(defconst jai-number-rx
+ (rx (and
+ symbol-start
+ (or (and (+ digit) (opt (and (any "eE") (opt (any "-+")) (+ digit))))
+ (and "0" (any "xX") (+ hex-digit)))
+ (opt (and (any "_" "A-Z" "a-z") (* (any "_" "A-Z" "a-z" "0-9"))))
+ symbol-end)))
+
+(defun jai-syntax-propertize-function (start end)
+ "Mark all heredoc regions as strings in the buffer."
+ (goto-char start)
+ ;; If we're already inside a herestring we have to take care of that one first
+ (when-let* ((ppss (syntax-ppss))
+ (inside (eq t (nth 3 ppss)))
+ (start-pos (nth 8 ppss))
+ (tag (get-text-property start-pos 'here-string-marker)))
+ (when (re-search-forward (concat "^[[:space:]]*" (regexp-quote tag) "[[:punct:]]*.*$") end 'move)
+ (let ((end (match-end 0)))
+ (put-text-property (1- end) end 'syntax-table (string-to-syntax "|"))
+ )
+ ))
+ (while (re-search-forward "#string +\\([a-zA-Z_][a-zA-Z0-9_]+\\)" end 'move)
+ (unless (nth 4 (syntax-ppss))
+ (let ((tag (match-string 1))
+ (beg (match-beginning 1)))
+ (unless (string= tag "CODE")
+ (put-text-property beg (1+ beg) 'here-string-marker tag)
+ (put-text-property beg (1+ beg) 'syntax-table (string-to-syntax "|"))
+ (when (re-search-forward (concat "^[[:space:]]*" "\\(" (regexp-quote tag) "\\)" "\\([[:space:]]*[[:punct:]]*.*$\\)") end 'move)
+ ;; Apply string syntax to everything between the start and end of heredoc
+ (let ((end (match-end 1)))
+ (put-text-property (1- end) end 'syntax-table (string-to-syntax "|"))
+ ))))
+
+ )
+ )
+ )
+
+(defun jai-postfix-cast-syntax (limit)
+ "Font-lock matcher for cast syntax: foo.(Type).
+Matches the .( and the closing ) with balanced parens in between,
+and highlights the type name inside."
+ (let ((found nil))
+ (while (and (not found) (re-search-forward "\\.(" limit t))
+ (let ((open-paren-pos (1- (point)))
+ (cast-start (match-beginning 0)))
+ (save-excursion
+ (goto-char open-paren-pos)
+ ;; Use forward-sexp to find the matching closing paren
+ (condition-case nil
+ (progn
+ (forward-sexp 1)
+ (let ((close-paren-pos (point))
+ (inner-start (1+ open-paren-pos))
+ (inner-end (1- (point)))
+ (type-start nil)
+ (type-end nil))
+ ;; Look for the last word in the cast expression
+ (goto-char inner-start)
+ (while (re-search-forward "\\([[:word:]]+\\)" inner-end t)
+ (setq type-start (match-beginning 1))
+ (setq type-end (match-end 1)))
+ ;; Store the positions for font-lock
+ (set-match-data (list cast-start close-paren-pos
+ cast-start (+ cast-start 2) ; subgroup 1: .(
+ (1- close-paren-pos) close-paren-pos ; subgroup 2: )
+ type-start type-end)) ; subgroup 3: Type (or nil if no word found)
+ (setq found t)))
+ (error nil))))) ; If forward-sexp fails, continue searching
+ found))
+
+(defconst jai-font-lock-defaults
+ `(;; .() Cast syntax - put this FIRST so it has priority
+ ;; Matching the type inside of .() is hacky.
+ ;; It breaks apart with things like foo.(get_type(baz=bark))
+ (jai-postfix-cast-syntax
+ (1 font-lock-keyword-face)
+ (2 font-lock-keyword-face)
+ (3 font-lock-type-face))
+
+ ;; Keywords
+ (,(jai-keywords-rx jai-keywords) 1 font-lock-keyword-face)
+
+ ;; single quote characters
+ ("\\('[[:word:]]\\)\\>" 1 font-lock-constant-face)
+
+ ;; Variables
+ (,(jai-keywords-rx jai-builtins) 1 font-lock-variable-name-face)
+
+ ;; Hash directives
+ ("#\\w+" . font-lock-preprocessor-face)
+
+ ;; At notes
+ ("@\\w+" . font-lock-preprocessor-face)
+
+ ;; Strings
+ ("\\\".*\\\"" . font-lock-string-face)
+
+ ;; Numbers
+ (,(jai-wrap-word-rx jai-number-rx) . font-lock-constant-face)
+
+ ;; Procedure names
+ ("\\([[:word:]]+\\)[[:space:]]*:[[:space:]]*:?[[:space:]]*\\(inline\\|#type\\)?[[:space:]]*\(" 1 font-lock-function-name-face)
+
+ ;; Types
+
+ (,(jai-keywords-rx jai-typenames) 1 font-lock-type-face)
+
+ (,jai-dollar-type-rx 1 font-lock-type-face)
+
+ ;; Foo.{} and bar.[] matching
+ ("\\([[:word:]]+\\)\\.\\(\{\\|\\[\\)" 1 font-lock-type-face)
+
+ ("\\([[:word:]]+\\)[[:space:]]*:[[:space:]]*:[[:space:]]*\\(struct\\|enum\\|union\\|#type,\\)" 1 font-lock-type-face)
+ ;; TODO: This detects false-positives in case of `for it_index, it: foo`, it thinks that foo is a type.
+ ;; Emacs regexes do not support negative lookaheads, so I'd need to add proper logic to jai-syntax-propertize-function
+ ;; but it's too hard for now. Oh well!
+ ("[[:word:]]+[[:space:]]*:[[:space:]]*\\**\\(\\[[[:word:]]*\\]\\|\\[\\.\\.\\]\\)?*[[:space:]]*\\**[[:space:]]*\\([[:word:]]+\\)" 2 font-lock-type-face)
+
+ ("---" . font-lock-constant-face)
+ ))
+
+;; add setq-local for older emacs versions
+(unless (fboundp 'setq-local)
+ (defmacro setq-local (var val)
+ `(set (make-local-variable ',var) ,val)))
+
+(defconst jai--defun-rx "\(.*\).*\{")
+
+(defmacro jai-paren-level ()
+ `(car (syntax-ppss)))
+
+(defun jai-line-is-defun ()
+ "return t if current line begins a procedure"
+ (interactive)
+ (save-excursion
+ (beginning-of-line)
+ (let (found)
+ (while (and (not (eolp)) (not found))
+ (if (looking-at jai--defun-rx)
+ (setq found t)
+ (forward-char 1)))
+ found)))
+
+(defun jai-beginning-of-defun ()
+ "Go to line on which current function starts."
+ (interactive)
+ (let ((orig-level (jai-paren-level)))
+ (while (and
+ (not (jai-line-is-defun))
+ (not (bobp))
+ (> orig-level 0))
+ (setq orig-level (jai-paren-level))
+ (while (>= (jai-paren-level) orig-level)
+ (skip-chars-backward "^{")
+ (backward-char))))
+ (when (jai-line-is-defun)
+ (beginning-of-line)))
+
+(defun jai-end-of-defun ()
+ "Go to line on which current function ends."
+ (interactive)
+ (let ((orig-level (jai-paren-level)))
+ (when (> orig-level 0)
+ (jai-beginning-of-defun)
+ (end-of-line)
+ (setq orig-level (jai-paren-level))
+ (skip-chars-forward "^}")
+ (while (>= (jai-paren-level) orig-level)
+ (skip-chars-forward "^}")
+ (forward-char)))))
+
+(defalias 'jai-parent-mode
+ (if (fboundp 'prog-mode) 'prog-mode 'fundamental-mode))
+
+;; imenu hookup
+(add-hook 'jai-mode-hook
+ (lambda ()
+ (setq imenu-generic-expression
+ '(("type" "^\\(.*:*.*\\) : " 1)
+ ("function" "^\\(.*\\) :: " 1)
+ ("struct" "^\\(.*\\) *:: *\\(struct\\)\\(.*\\){" 1)))))
+
+;; NOTE: taken from the scala-indent package and modified for Jai.
+;; Still uses the js-indent-line as a base, which will have to be
+;; replaced when the language is more mature.
+(defun jai--indent-on-parentheses ()
+ (when (and (= (char-syntax (char-before)) ?\))
+ (= (save-excursion (back-to-indentation) (point)) (1- (point))))
+ (js-indent-line)))
+
+(defun jai--add-self-insert-hooks ()
+ (add-hook 'post-self-insert-hook
+ 'jai--indent-on-parentheses))
+
+;;;###autoload
+(define-derived-mode jai-mode jai-parent-mode "Jai"
+ :syntax-table jai-mode-syntax-table
+ :group 'jai
+ (setq bidi-paragraph-direction 'left-to-right)
+ (setq-local require-final-newline mode-require-final-newline)
+ (setq-local parse-sexp-ignore-comments t)
+ (setq-local comment-start-skip "\\(//+\\|/\\*+\\)\\s *")
+ (setq-local comment-start "//")
+ (setq-local block-comment-start "/*")
+ (setq-local block-comment-end "*/")
+ (setq-local indent-line-function 'js-indent-line)
+ (setq-local font-lock-defaults '(jai-font-lock-defaults))
+ (setq-local beginning-of-defun-function 'jai-beginning-of-defun)
+ (setq-local end-of-defun-function 'jai-end-of-defun)
+ (setq-local syntax-propertize-function 'jai-syntax-propertize-function)
+ ;; add indent functionality to some characters
+ (jai--add-self-insert-hooks)
+
+ (font-lock-ensure))
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.jai\\'" . jai-mode))
+
+(defconst jai--error-regexp
+ "\\([^ \n:]+.*\.jai\\):\\([0-9]+\\),\\([0-9]+\\):")
+(push `(jai ,jai--error-regexp 1 2 3 2) compilation-error-regexp-alist-alist)
+(push 'jai compilation-error-regexp-alist)
+
+(provide 'jai-mode)
+;;; jai-mode.el ends here