From 8baa36909a37740d81d7dec11e6eac32fc579f8c Mon Sep 17 00:00:00 2001 From: annabunches Date: Sun, 8 Aug 2021 22:27:26 -0400 Subject: [PATCH] Add kOS config --- emacs/.emacs | 4 + emacs/local/kos-mode.el | 325 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100755 emacs/local/kos-mode.el diff --git a/emacs/.emacs b/emacs/.emacs index 7e79ec2..f7cdfd9 100755 --- a/emacs/.emacs +++ b/emacs/.emacs @@ -55,3 +55,7 @@ ;; Use the right mode for Arduino sketches (add-to-list 'auto-mode-alist '("\\.ino\\'" . c++-mode)) + +(add-to-list 'load-path "~/.emacs.d/local/") +(require 'kos-mode) +(setq kos-indent 2) diff --git a/emacs/local/kos-mode.el b/emacs/local/kos-mode.el new file mode 100755 index 0000000..7c2746f --- /dev/null +++ b/emacs/local/kos-mode.el @@ -0,0 +1,325 @@ +;;; kos-mode.el --- KSP kOS KerboScript major mode + +;; Author: Charlie Green +;; URL: https://github.com/charliegreen/kos-mode +;; Version: 0.2.1 + +;;; Commentary: + +;; A major mode for editing KerboScript program files, from the Kerbal Space Program mod +;; kOS. I hope this is useful for someone! + +;; TODO: +;; features: +;; * potentially add custom faces for strings/comments +;; * add AGn action group highlighting +;; * add useful interactive commands (eg electric braces) +;; other: +;; * yikes, clean up some of the regexes here.... + +;; TODO (long-term): +;; * make sure I got the syntax right and included all global functions and variables +;; * add some nice completion thing for completing fields of data structures (eg +;; SHIP:VELOCITY autofills ":SURFACE", which autofills ":MAG", etc) +;; * deal with highlighting function calls vs global variables, especially when they +;; have the same name (eg STAGE) +;; * add an optional auto-formatter + +;;; Code: + +(defgroup kos-mode () + "Options for `kos-mode'." + :group 'languages) + +(defgroup kos-mode-faces () + "Faces used by `kos-mode'." + :group 'kos-mode) + +(defcustom kos-indent (default-value 'tab-width) + "Basic indent increment for indenting kOS code." + :group 'kos-mode) + +(defface kos-keyword-face + '((t :inherit (font-lock-keyword-face))) + "Face for keywords." + :group 'kos-mode-faces) + +(defface kos-operator-face + '((t :inherit (font-lock-builtin-face))) + "Face for operators." + :group 'kos-mode-faces) + +(defface kos-global-face + '((t :inherit (font-lock-constant-face))) + "Face for globally defined variables." + :group 'kos-mode-faces) + +(defface kos-constant-face + '((t :inherit (font-lock-constant-face))) + "Face for constants." + :group 'kos-mode-faces) + +(defface kos-function-name-face + '((t :inherit (font-lock-function-name-face))) + "Face for highlighting the names of functions in their definitions." + :group 'kos-mode-faces) + +(defmacro kos--opt (keywords) + "Compile a regex matching any of KEYWORDS." + `(eval-when-compile + (regexp-opt ,keywords 'words))) + +(eval-and-compile + (defconst kos-keywords + '("add" "all" "at" "batch" "break" "clearscreen" "compile" "copy" "declare" + "delete" "deploy" "do" "do" "edit" "else" "file" "for" "from" "from" + "function" "global" "if" "in" "is" "local" "lock" "log" "off" "on" + "once" "parameter" "preserve" "print" "reboot" "remove" "rename" "run" + "set" "shutdown" "stage" "step" "switch" "then" "to" "toggle" "unlock" + "unset" "until" "volume" "wait" "when" "return" "lazyglobal"))) + +(eval-and-compile + (defconst kos-globals + '("ship" "target" "hastarget" "heading" "prograde" "retrograde" "facing" + "maxthrust" "velocity" "geoposition" "latitude" "longitude" "up" "north" + "body" "angularmomentum" "angularvel" "angularvelocity" "mass" + "verticalspeed" "groundspeed" "surfacespeed" "airspeed" "altitude" + "apoapsis" "periapsis" "sensors" "srfprograde" "srfretrograde" "obt" + "status" "shipname" + + "terminal" "core" "archive" "nextnode" "hasnode" "allnodes" + + "liquidfuel" "oxidizer" "electriccharge" "monopropellant" "intakeair" + "solidfuel" + + "alt" "eta" "encounter" + + "sas" "rcs" "gear" "lights" "brakes" "abort" "legs" "chutes" "chutessafe" + "panels" "radiators" "ladders" "bays" "intakes" "deploydrills" "drills" + "fuelcells" "isru" "ag1" "ag2" "ag3" "ag4" "ag5" "ag6" "ag7" "ag8" "ag9" + "ag10" + + "throttle" "steering" "wheelthrottle" "wheelsteering" + + "missiontime" "version" "major" "minor" "build" "sessiontime" + "homeconnection" "controlconnection" + + "kuniverse" "config" "warp" "warpmode" "mapview" "loaddistance" + "solarprimevector" "addons" + + "red" "green" "blue" "yellow" "cyan" "magenta" "purple" "white" "black"))) + +(eval-and-compile + (defconst kos-functions + '("round" "mod" "abs" "ceiling" "floor" "ln" "log10" "max" "min" "random" "sqrt" + "char" "unchar" "sin" "cos" "tan" "arcsin" "arccos" "arctan" "arctan2" + + "list" "rgb" "rgba" "hsv" "hsva" + "clearscreen" "stage" "constant" "profileresult"))) + +(eval-and-compile + (defconst kos-constants + '("pi" "e" "g" "c" "atmtokpa" "kpatoatm" "degtorad" "radtodeg"))) + +(defun kos--opt-nomember (keywords) + "Compile a regex matching any of KEYWORDS with no leading colon. + +This is the same as `kos--opt', except it won't match any of +KEYWORDS if they are being accessed as a structure member (eg, +for `(kos--opt-nomember '(\"foo\"))`, 'foo' would be highlighted, +but 'bar:foo' would not)." + (concat "\\(?:^\\|[^:]\\)" (kos--opt keywords))) + +(defconst kos-font-lock-keywords + ;; aren't these regexes beautiful? + `((,(kos--opt-nomember kos-keywords) 1 'kos-keyword-face) + + (,(kos--opt-nomember kos-globals) 1 'kos-global-face) + (,(kos--opt-nomember kos-constants) 1 'kos-constant-face) + + ;; for numbers; have this before operators so decimals are still highlighted + ("\\b[[:digit:].]+\\(e[+-]?[:digit:]+\\)?\\b" . 'kos-constant-face) + + ;; ((rx (any ?+ ?- ?* ?/ ?^ ?( ?))) . 'kos-operator-face) ; arithmetic ops + ("\\+\\|-\\|\\*\\|/\\|\\^\\|(\\|)" . 'kos-operator-face) ; arithmetic ops + + ;; ((rx word-boundary + ;; (or "not" "and" "or" "true" "false" "<>" "<=" ">=" "=" ">" "<") + ;; word-boundary) 1 'kos-operator-face) ; logical ops + ("\\b\\(not\\|and\\|or\\|true\\|false\\|<>\\|<=\\|>=\\|=\\|>\\|<\\)\\b" ; logical ops + 1 'kos-operator-face) + + ;;((rx (any ?{ ?} ?[ ?] ?, ?. ?: ?@)) . 'kos-operator-face) ; other ops + ("{\\|}\\|\\[\\|\\]\\|,\\|\\.\\|:\\|@" . 'kos-operator-face) ; other ops + + ;; highlight function declarations + ("\\bfunction\\s-+\\([[:alpha:]_][[:alnum:]_]*\\)" 1 'kos-function-name-face)) + "Keyword highlighting specification for `kos-mode'.") + +;; (defvar kos-mode-map +;; (let ((map (make-sparse-keymap))) +;; ;(define-key map [foo] 'kos-do-foo) +;; map) +;; "Keymap for `kos-mode'.") + +(defvar kos-mode-syntax-table + (let ((st (make-syntax-table))) + (modify-syntax-entry ?/ ". 12" st) ; // starts comments + (modify-syntax-entry ?\n ">" st) ; newline ends comments + st) + "Syntax table for `kos-mode'.") + +;; https://web.archive.org/web/20070702002238/http://two-wugs.net/emacs/mode-tutorial.html +(defun kos-indent-line () + "Indent the current line of kOS code." + (interactive) + + (require 'cl-lib) + + (let ((not-indented t) cur-indent + (r-bob "^[^\n]*?{[^}]*$") ; beginning of block regex + (r-nl "\\(?:\n\\|\r\n\\|$\\)") ; an actual newline ($ was acting funky) + (ltss nil)) ; lines to start of (unterminated) statement; see `in-unterminated-p' + + (cl-flet* ((get-cur-line () + (save-excursion + (let ((start (progn (beginning-of-line) (point))) + (end (progn (end-of-line) (point)))) + (buffer-substring-no-properties start end)))) + + (back-to-nonblank-line () + (let ((cont t)) + (while cont + (forward-line -1) + (if (or (bobp) (not (looking-at "^\\s-*$"))) + (setq cont nil))))) + + (set-indent (v &optional not-relative) + (setq cur-indent (if not-relative v + (+ (current-indentation) + (* v kos-indent)))) + (setq not-indented nil)) + + (strip-text + (s) ;; remove strings and comments + (while (string-match + (rx (: ?\" (0+ (or (: ?\\ ?\") + (not (any ?\")))) ?\")) s) + (setq s (replace-match "" t t s))) + (while (string-match (concat "//.*" r-nl) s) + (setq s (replace-match "" t t s))) s) + + (in-unterminated-p + () + ;; Returns nil if in unterminated statement and the number of + ;; lines back to the beginning of the statement otherwise + (setq ltss nil) + (save-excursion + (let ((loopp t) ; whether we should keep searching + (rettp nil) ; whether we'll return a positive value + (count 0) ; number of lines we've processed before + (found-end-p nil) ; whether we've found a terminating line + cur-line) ; the current line we're processing + (while (and loopp (not (bobp))) + (setq cur-line (strip-text (get-cur-line))) + (cond + ;; found an unterminated statement + ((string-match + (concat "^\\s-*" (kos--opt kos-keywords) "\\b\\s-*[^{.]*$") + cur-line) + (setq loopp nil rettp t)) + + ;; found a block opener + ((string-match "^\\s-*{[^}]*$" cur-line) + (setq loopp nil)) + + ;; found a block closer (which counts as terminator, + ;; since it presumably has an opener) or a line ending + ;; with a dot + ((or + (string-match ".*\\.\\s-*$" cur-line) + (string-match "^\\s-*}[^{]*$" cur-line)) + + ;; check if count is zero so we don't indent whitespace + ;; past the terminator + (if (and (not found-end-p) (zerop count)) + (setq found-end-p t) + (setq loopp nil)))) + + ;; at end of each loop, if we don't want to break, update + ;; counter and move POINT to next line to process + (if loopp + (progn + (setq count (1+ count)) + (forward-line -1)))) + (if (not rettp) nil + (progn + (setq ltss count) + (not (= count 0))))))) + + ;; for "looking-at-line" + (lal (r) (string-match r (strip-text (get-cur-line))))) + + (save-excursion + (beginning-of-line) + + (cond ((bobp) (set-indent 0 t)) ; if at beginning of buffer, indent to 0 + ((lal "^[ \t]*}") ; if closing a block + (progn ; then indent one less than previous line + ;; TODO: check if previous line is part of a line continuation + (back-to-nonblank-line) + (cond + ((looking-at r-bob) (set-indent 0)) ; if closing empty block, match it + ((in-unterminated-p) ; if last line unterminated, indent back two + (set-indent -2)) + (t (set-indent -1))))) ; otherwise, indent back one + + ((lal "^[ \t]*{") ; if opening a block on a blank line + (progn ; then indent the same as last line + (back-to-nonblank-line) + (set-indent 0))) + + ((in-unterminated-p) ; if line part of unterminated statement + (progn ; then indent one more than beginning + (forward-line (- ltss)) + (set-indent +1))) + + (t (while not-indented ; else search backwards for clues + (back-to-nonblank-line) + (cond + ((bobp) (setq not-indented nil)) ; perhaps we won't find anything + ((lal "^[ \t]*}[^{]*$") (set-indent 0)) ; found the end of a block + ((lal r-bob) (set-indent +1)))))))) ; found the beginning of a block + + (if (not cur-indent) (setq cur-indent 0)) + (if (< cur-indent 0) (setq cur-indent 0)) + + ;; now actually indent to cur-indent + (if (save-excursion ; if within indentation + (let ((point (point)) + (start (progn (beginning-of-line) (point))) + (end (progn (back-to-indentation) (point)))) + (and (<= start point) (<= point end)))) + (indent-line-to cur-indent) ; then indent line and move point + (save-excursion (indent-line-to cur-indent))))) ; else just indent line + +;;;###autoload +(define-derived-mode kos-mode prog-mode "KerboScript" + "Major mode for editing kOS program files, for the game Kerbal Space Program." + :syntax-table kos-mode-syntax-table + (make-local-variable 'comment-start) + (make-local-variable 'comment-start-skip) + (make-local-variable 'font-lock-defaults) + (make-local-variable 'indent-line-function) + (setq comment-start "// ") + (setq comment-start-skip "//+\\s-*") + (setq font-lock-defaults + '(kos-font-lock-keywords nil t)) ; t makes this case-insensitive + (setq indent-line-function 'kos-indent-line)) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.ks\\'" . kos-mode)) + +(provide 'kos-mode) + +;;; kos-mode.el ends here