commit b4d7e1570acad7dd3b10944bc4b0a5712a088c32
parent 41bf5657c4b4fcb8e35def26a7b878d6be7db4f9
Author: Vincent Demeester <vincent@sbr.pm>
Date: Wed, 6 Apr 2022 03:05:21 +0200
tools/emacs: add a cue-mode.el
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Diffstat:
1 file changed, 386 insertions(+), 0 deletions(-)
diff --git a/tools/emacs/lisp/cue-mode.el b/tools/emacs/lisp/cue-mode.el
@@ -0,0 +1,386 @@
+;;; cue-mode.el --- CUE Lang Major Mode -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021 Russell Sim
+
+;; Author: Russell Sim <russell.sim@gmail.com>
+;; Keywords: languages
+
+;; 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:
+
+;;
+
+;;; Code:
+
+(require 'smie)
+(require 'cl-extra)
+
+(defgroup cue '()
+ "Major mode for editing Cue files."
+ :group 'languages)
+
+(defcustom cue-eval-command
+ '("cue" "eval")
+ "Cue command to run in ‘cue-eval-buffer’.
+See also: `cue-command-options'."
+ :type '(repeat string)
+ :group 'cue)
+
+(defcustom cue-command-options
+ '()
+ "A list of options and values to pass to `cue-command'.
+For example:
+ '(\"-e\" \"Nginx.Out\" \"--out=text\")"
+ :group 'cue
+ :type '(repeat string))
+
+(defcustom cue-fmt-command
+ '("cue" "fmt")
+ "Cue format command."
+ :type '(repeat string)
+ :group 'cue)
+
+
+(defcustom cue-library-search-directories
+ nil "Sequence of Cue package search directories."
+ :type '(repeat directory)
+ :group 'cue)
+
+(defcustom cue-indent-level
+ 4
+ "Number of spaces to indent with."
+ :type '(number)
+ :group 'cue)
+
+(defvar cue--identifier-regexp
+ "[a-zA-Z_][a-zA-Z0-9_]*"
+ "Regular expression matching a Cue identifier.")
+
+(defvar cue-font-lock-keywords
+ (let ((builtin-regex (regexp-opt '("package" "import" "for" "in" "if" "let") 'words))
+ (constant-regex (regexp-opt '("false" "null" "true") 'words))
+ ;; All builtin functions (see https://cue.org/docs/references/spec/#builtin-functions)
+ (standard-functions-regex (regexp-opt '("len" "close" "and" "or" "div" "mod" "quo" "rem") 'words)))
+ (list
+ `(,builtin-regex . font-lock-builtin-face)
+ `(,constant-regex . font-lock-constant-face)
+ ;; identifiers starting with a # or _ are reserved for definitions
+ ;; and hidden fields
+ `(,(concat "_?#" cue--identifier-regexp "+:?") . font-lock-type-face)
+ `(,(concat cue--identifier-regexp "+:") . font-lock-keyword-face)
+ ;; all identifiers starting with __(double underscores) as keywords
+ `(,(concat "__" cue--identifier-regexp) . font-lock-keyword-face)
+ `(,standard-functions-regex . font-lock-function-name-face)))
+ "Minimal highlighting for ‘cue-mode’.")
+
+(defun cue-syntax-stringify ()
+ "Put `syntax-table' property correctly on single/triple quotes."
+ (let* ((ppss (save-excursion (backward-char 3) (syntax-ppss)))
+ (string-start (and (eq t (nth 3 ppss)) (nth 8 ppss)))
+ (quote-starting-pos (- (point) 3))
+ (quote-ending-pos (point)))
+ (cond ((or (nth 4 ppss) ;Inside a comment
+ (and string-start
+ ;; Inside of a string quoted with different triple quotes.
+ (not (eql (char-after string-start)
+ (char-after quote-starting-pos)))))
+ ;; Do nothing.
+ nil)
+ ((nth 5 ppss)
+ ;; The first quote is escaped, so it's not part of a triple quote!
+ (goto-char (1+ quote-starting-pos)))
+ ((null string-start)
+ ;; This set of quotes delimit the start of a string.
+ (put-text-property quote-starting-pos (1+ quote-starting-pos)
+ 'syntax-table (string-to-syntax "|")))
+ (t
+ ;; This set of quotes delimit the end of a string.
+ (put-text-property (1- quote-ending-pos) quote-ending-pos
+ 'syntax-table (string-to-syntax "|"))))))
+
+
+(defvar cue-smie-verbose-p nil
+ "Emit context information about the current syntax state.")
+
+(defmacro cue-smie-debug (message &rest format-args)
+ `(progn
+ (when cue-smie-verbose-p
+ (message (format ,message ,@format-args)))
+ nil))
+
+(defun verbose-cue-smie-rules (kind token)
+ (let ((value (cue-smie-rules kind token)))
+ (cue-smie-debug "%s '%s'; sibling-p:%s prev-is-OP:%s hanging:%s == %s" kind token
+ (ignore-errors (smie-rule-sibling-p))
+ (ignore-errors (smie-rule-prev-p "OP"))
+ (ignore-errors (smie-rule-hanging-p))
+ value)
+ value))
+
+(defvar cue-smie-grammar
+ (smie-prec2->grammar
+ (smie-merge-prec2s
+ (smie-bnf->prec2
+ '((exps (exp "," exp))
+ (exp (field)
+ ("import" id)
+ ("package" id)
+ (id))
+ (field (id ":" exp))
+ (id))
+ '((assoc ",") (assoc "\n") (left ":") (left ".") (left "let"))
+ '((right "=")))
+
+ (smie-precs->prec2
+ '((right "=")
+ (left "||" "|")
+ (left "&&" "&")
+ (nonassoc "=~" "!~" "!=" "==" "<=" ">=" "<" ">")
+ (left "+" "-")
+ (left "*" "/"))))))
+
+;; Operators
+;; + && == < = ( )
+;; - || != > : { }
+;; * & =~ <= ? [ ] ,
+;; / | !~ >= ! _|_ ... .
+;; _|_ bottom
+
+(defun cue-smie-rules (kind token)
+ (pcase (cons kind token)
+ (`(:elem . basic) smie-indent-basic)
+ (`(,_ . ",") (cue-smie--indent-nested))
+ (`(,_ . "}") (cue-smie--indent-closing))
+ (`(,_ . "]") (smie-rule-parent (- 0 cue-indent-level)))
+ (`(,_ . ")") (smie-rule-parent (- 0 cue-indent-level)))
+ ))
+
+(defun cue-smie--in-object-p ()
+ "Return t if the current block we are in is wrapped in {}."
+ (let ((ppss (syntax-ppss)))
+ (or (null (nth 1 ppss))
+ (and (nth 1 ppss)
+ (or
+ (eq ?{ (char-after (nth 1 ppss)))
+ (eq ?\( (char-after (nth 1 ppss))))))))
+
+
+(defun cue-smie-backward-token ()
+ (let ((pos (point)))
+ (forward-comment (- (point)))
+ (cond
+ ((and (not (eq (char-before) ?\,)) ;Coalesce ";" and "\n".
+ (> pos (line-end-position))
+ (cue-smie--in-object-p))
+ (skip-chars-forward " \t")
+ ;; Why bother distinguishing \n and ,?
+ ",") ;;"\n"
+ (t
+ (buffer-substring-no-properties
+ (point)
+ (progn (if (zerop (skip-syntax-backward "."))
+ (skip-syntax-backward "w_'"))
+ (point)))))))
+
+
+(defun cue-smie-forward-token ()
+ (skip-chars-forward " \t")
+ (cond
+ ((and (looking-at "[\n]")
+ (or (save-excursion (skip-chars-backward " \t")
+ ;; Only add implicit , when needed.
+ (or (bolp) (eq (char-before) ?\,)))
+ (cue-smie--in-object-p)))
+ (if (eolp) (forward-char 1) (forward-comment 1))
+ ;; Why bother distinguishing \n and ;?
+ ",") ;;"\n"
+ ((progn (forward-comment (point-max)) nil))
+ (t
+ (buffer-substring-no-properties
+ (point)
+ (progn (if (zerop (skip-syntax-forward "."))
+ (skip-syntax-forward "w_'"))
+ (point))))))
+
+(defun cue-smie--indent-nested ()
+ (let ((ppss (syntax-ppss)))
+ (if (nth 1 ppss)
+ (let ((parent-indentation (save-excursion
+ (goto-char (nth 1 ppss))
+ (back-to-indentation)
+ (current-column))))
+ (cons 'column (+ parent-indentation cue-indent-level))))))
+
+(defun cue-smie--indent-closing ()
+ (let ((ppss (syntax-ppss)))
+ (if (nth 1 ppss)
+ (let ((parent-indentation (save-excursion
+ (goto-char (nth 1 ppss))
+ (back-to-indentation)
+ (current-column))))
+ (cons 'column parent-indentation)))))
+
+(defvar cue-syntax-propertize-function
+ (syntax-propertize-rules
+ ((rx (or "\"\"\"" "'''"))
+ (0 (ignore (cue-syntax-stringify))))))
+
+(defconst cue-mode-syntax-table
+ (let ((table (make-syntax-table)))
+ ;; Comments. Cue supports /* */ and // as comment delimiters
+ (modify-syntax-entry ?/ ". 124" table)
+ ;; Additionally, Cue supports # as a comment delimiter
+ (modify-syntax-entry ?\n ">" table)
+ ;; ", ', ,""" and ''' are quotations in Cue.
+ ;; both """ and ''' are handled by cue--syntax-propertize-function
+ (modify-syntax-entry ?' "\"" table)
+ (modify-syntax-entry ?\" "\"" table)
+ ;; Our parenthesis, braces and brackets
+ (modify-syntax-entry ?\( "()" table)
+ (modify-syntax-entry ?\) ")(" table)
+ (modify-syntax-entry ?\{ "(}" table)
+ (modify-syntax-entry ?\} "){" table)
+ (modify-syntax-entry ?\[ "(]" table)
+ (modify-syntax-entry ?\] ")[" table)
+ table)
+ "Syntax table for `cue-mode'.")
+
+
+;;;###autoload
+(define-derived-mode cue-mode prog-mode "CUE Lang Mode"
+ :syntax-table cue-mode-syntax-table
+ (setq-local font-lock-defaults '(cue-font-lock-keywords ;; keywords
+ nil ;; keywords-only
+ nil ;; case-fold
+ nil ;; syntax-alist
+ nil ;; syntax-begin
+ ))
+
+ (setq-local syntax-propertize-function
+ cue-syntax-propertize-function)
+
+ ;; cue lang uses tabs for indent by default
+ (setq-local indent-tabs-mode t)
+ (setq-local tab-width cue-indent-level)
+
+ (smie-setup cue-smie-grammar 'verbose-cue-smie-rules
+ :forward-token #'cue-smie-forward-token
+ :backward-token #'cue-smie-backward-token)
+ (setq-local smie-indent-basic cue-indent-level)
+ (setq-local smie-indent-functions '(smie-indent-fixindent
+ smie-indent-bob
+ smie-indent-comment
+ smie-indent-comment-continue
+ smie-indent-comment-close
+ smie-indent-comment-inside
+ smie-indent-keyword
+ smie-indent-after-keyword
+ smie-indent-empty-line
+ smie-indent-exps))
+
+ (setq-local comment-start "// ")
+ (setq-local comment-start-skip "//+[\t ]*")
+ (setq-local comment-end "")
+ )
+
+;;;###autoload
+(add-to-list 'auto-mode-alist (cons "\\.cue\\'" 'cue-mode))
+
+;;;###autoload
+(defun cue-eval-buffer ()
+ "Run cue with the path of the current file."
+ (interactive)
+ (let ((file-to-eval (file-truename (buffer-file-name)))
+ (search-dirs cue-library-search-directories)
+ (output-buffer-name "*cue output*"))
+ (save-some-buffers (not compilation-ask-about-save)
+ (let ((directories (cons (file-name-directory file-to-eval)
+ search-dirs)))
+ (lambda ()
+ (member (file-name-directory (file-truename (buffer-file-name)))
+ directories))))
+ (let ((cmd (car cue-eval-command))
+ (args (append (cdr cue-eval-command)
+ cue-command-options
+ (cl-loop for dir in search-dirs
+ collect "-I"
+ collect dir)
+ (list file-to-eval))))
+ (let ((outbuf (get-buffer-create output-buffer-name)))
+ (with-current-buffer outbuf
+ (let ((origional-point (point)))
+ (setq buffer-read-only nil)
+ (erase-buffer)
+ (if (zerop (apply #'call-process cmd nil t nil args))
+ (progn
+ (cue-mode)
+ (view-mode))
+ (compilation-mode nil))
+ (goto-char origional-point)))
+ (display-buffer outbuf '(nil (allow-no-window . t)))))))
+
+(define-key cue-mode-map (kbd "C-c C-c") 'cue-eval-buffer)
+
+
+;;;###autoload
+(defun cue-reformat-buffer ()
+ "Reformat entire buffer using the Cue format utility."
+ (interactive)
+ (let ((point (point))
+ (file-name (buffer-file-name))
+ (stdout-buffer (get-buffer-create "*cue fmt stdout*"))
+ (stderr-buffer-name "*cue fmt stderr*")
+ (stderr-file (make-temp-file "cue fmt")))
+ (when-let ((stderr-window (get-buffer-window stderr-buffer-name t)))
+ (quit-window nil stderr-window))
+ (unwind-protect
+ (let* ((only-test buffer-read-only)
+ (exit-code (apply #'call-process-region nil nil (car cue-fmt-command)
+ nil (list stdout-buffer stderr-file) nil
+ (append (cdr cue-fmt-command)
+ (when only-test '("--test"))
+ '("-")))))
+ (cond ((zerop exit-code)
+ (progn
+ (if (or only-test
+ (zerop (compare-buffer-substrings nil nil nil stdout-buffer nil nil)))
+ (message "No format change necessary.")
+ (erase-buffer)
+ (insert-buffer-substring stdout-buffer)
+ (goto-char point))
+ (kill-buffer stdout-buffer)))
+ ((and only-test (= exit-code 2))
+ (message "Format change is necessary, but buffer is read-only."))
+ (t (with-current-buffer (get-buffer-create stderr-buffer-name)
+ (setq buffer-read-only nil)
+ (insert-file-contents stderr-file t nil nil t)
+ (goto-char (point-min))
+ (when file-name
+ (while (search-forward "<stdin>" nil t)
+ (replace-match file-name)))
+ (set-buffer-modified-p nil)
+ (compilation-mode nil)
+ (display-buffer (current-buffer)
+ '((display-buffer-reuse-window
+ display-buffer-at-bottom
+ display-buffer-pop-up-frame)
+ .
+ ((window-height . fit-window-to-buffer))))))))
+ (delete-file stderr-file))))
+
+(define-key cue-mode-map (kbd "C-c C-r") 'cue-reformat-buffer)
+
+(provide 'cue-mode)
+;;; cue-mode.el ends here