From 8baa36909a37740d81d7dec11e6eac32fc579f8c Mon Sep 17 00:00:00 2001
From: annabunches <annabunches@gmail.com>
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