;;; ccscript --- Mode for editing CCScript files ;; (c) copyright 2001 Stefan Reuther ;; Author: Stefan Reuther ;; X-URL: http://phost.de/~stefan/pcc.html ;; 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 2, 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 [your] Emacs; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, ;; Boston, MA 02111-1307, USA. ;;; Commentary: ;; CCScript is the scripting language of Planets Command Center, ;; a player-side program for the play-by-email game `VGA Planets'. ;; This file provides a major mode for editing such files. ;; + some (not bullet-proof) font-lock-keywords ;; + indentation ;; + electric case correction ;; + comment syntax ;; In addition, it attempts to ensure that files are read/written ;; in MSDOS style, which is not required by PCC, but nice because ;; not all DOS programs handle Unix LFs gracefully. ;;; Usage: ;; (load "ccscript.el") ;; in ~/.emacs, and you're ready. This associates the extension `.q' ;; with CCScript mode. You can also M-x ccscript-mode manually. ;;; Bugs: ;; + Fontification is not bullet-proof (try `"foo%bar"'). ;;; History: ;; 20/Jan/2001 started ;; 20/May/2001 Name lists updated for PCC 1.0.11 ;; Bugfix: don't mess up case in literals/comments ;; Bugfix: parsing parenthesized expressions didn't work ;; 08/Dec/2002 Name lists updated from PCC 1.1.3 ;; 27/Oct/2004 Name lists updated from PCC 1.1.9 ;; Added some customize declarations (defconst ccscript-keywords '("Abort" "And" "Break" "Case" "Close" "Continue" "Dim" "Do" "Else" "End" "EndIf" "EndSelect" "EndSub" "EndTry" "EndWith" "For" "ForEach" "If" "Local" "Loop" "Mod" "Next" "Not" "On" "Open" "Option" "Optional" "Or" "Return" "RunHook" "Select" "Shared" "Static" "Stop" "Sub" "To" "Then" "Try" "Until" "While" "With" "Xor") "CCScript Keywords which are fontified and case-twiddled") (defconst ccscript-additional-keywords '("ATan" "Abort" "Abs" "AddCommand" "AddConfig" "AddFCode" "Asc" "Atom" "AtomStr" "AuthPlayer" "Autobuild" "Bind" "Break" "BuildBase" "BuildBaseDefense" "BuildBaseDefenseWait" "BuildDefense" "BuildDefenseWait" "BuildFactories" "BuildFactoriesWait" "BuildFighters" "BuildMines" "BuildMinesWait" "BuildShip" "BuildTorps" "CAdd" "CCompare" "CExtract" "CMul" "CRemove" "CSub" "CargoTransfer" "CargoTransferWait" "CargoUnload" "CargoUpload" "CargoUploadWait" "Cfg" "ChangeFleetLeader" "ChooseObject" "Chr" "Close" "Continue" "Cos" "CountPlanets" "CountShips" "CreateKeymap" "CreatePlanetProperty" "CreateShipProperty" "Delete" "Dim" "Distance" "Do" "End" "EnqueueShip" "Eval" "False" "FPos" "FSize" "FileWindow" "FindPlanet" "FindShip" "First" "FixShip" "FixShipWait" "For" "ForEach" "FreeFile" "Get" "GetByte" "GetStr" "GotoChart" "GotoScreen" "Help" "If" "InStr" "Input" "InputFCode" "Int" "IsEmpty" "IsNum" "IsSpecialFCode" "IsString" "Key" "Left" "Len" "ListShips" "Listbox" "Load" "LoadResource" "Lock" "Mark" "Max" "Message" "MessageBox" "Mid" "Min" "MoveTo" "MoveTowards" "NewCircle" "NewLine" "NewMarker" "NewRectangle" "Notify" "On" "Open" "Option" "PlanetAt" "PlanetName" "PopupConsole" "Print" "Put" "Random" "RecycleShip" "RecycleShipWait" "Rest" "Return" "Right" "Round" "RunHook" "SaveGame" "Search" "Seek" "SelectionExec" "SelectionLayer" "SelectionLoad" "SelectionSave" "SellSupplies" "SetByte" "SetColonistTax" "SetColor" "SetComment" "SetEnemy" "SetFCode" "SetFleet" "SetFleetName" "SetLong" "SetMission" "SetName" "SetNativeTax" "SetSpeed" "SetStr" "SetTech" "SetWaypoint" "SetWord" "ShipName" "Sin" "Sqr" "Stop" "Str" "StrCase" "String" "Sub" "Tan" "Trim" "True" "Truehull" "Try" "TryLoad" "UI" "Unmark" "Update" "UseKeymap" "Val" "WaitOneTurn" "With" "Z") "CCScript keywords which are not fontified, but case-twiddled The distinction of what goes in `ccscript-keywords' and what goes here is completely arbitrary.") ; the list below generated with ; grep ^[A-Z] /dos/p/vplanets/cc/doc/manual/prop.man | ; sed 's/(.*)//' | sed 's/, */./g' | tr . '\n' | grep -v '\$' | ; tr -d ' \r' | sort | uniq | sed 's/.*/"&"/' ; the list above uses commands.man and function.man (defconst ccscript-variable-keywords '("Action" "Adj" "Ammo" "Aux" "Base" "Bases" "Bays" "Beam" "Beams" "Build" "Capital" "Cargo" "Change" "Chart" "Class" "Colonists" "Color" "Comment" "Cost" "Count" "Crew" "D" "DX" "DY" "Damage" "Date" "Defense" "Density" "Directory" "Dist" "Ega" "End" "Enemy" "Engine" "Engines" "Err" "Eta" "Expire" "FCode" "Factories" "Fighter" "Fighters" "Fleet" "Free" "Freighters" "Fuel" "Full" "GUI" "GameDirectory" "GameType" "Gov" "Ground" "Happy" "Heading" "Host" "HostVersion" "Hull" "Hulls" "Id" "Id2" "Image" "InMsgs" "Industry" "Info1" "Info2" "Intercept" "Keep" "Kill" "LCount" "LMax" "LastScan" "Launchers" "Layer" "Left" "Loc" "Local" "M" "MC" "Magic" "Marked" "Mass" "Max" "MaxFuel" "Mined" "Mines" "Mission" "Money" "Move" "My" "N" "Name" "Natives" "Normal" "Orbit" "OutMsgs" "Own" "Owner" "PBPs" "Planet" "Planets" "Prefix" "Program" "QPos" "Race" "Radius" "RandomSeed" "Real" "RegStr1" "RegStr2" "Remainder" "Remote" "Result" "Right" "RootDirectory" "Scanned" "Score" "Screen" "Seed" "Selection" "Shape" "Shield" "Ship" "Ships" "Shipyard" "Short" "Sim" "Special" "Speed" "Status" "Storage" "Str" "Supplies" "System" "T" "Tag" "Tax" "Team" "Tech" "Temp" "Time" "Torp" "Torpedo" "Total" "Tow" "Transfer" "Turn" "Type" "UI" "Units" "Unload" "VCRs" "Version" "Visible" "Voltage" "Want" "Waypoint" "X" "Y" "YesNo") "Parts of predefined CCScript variable names, which are not fontified but case-twiddled if `ccscript-auto-caps-vars' is not nil") (defgroup ccscript nil "Options regarding the CCScript mode") (defcustom ccscript-basic-indent 2 "Basic indentation for CCScript mode" :group 'ccscript :type 'integer) (defcustom ccscript-auto-caps t "Automatically fix capitalisation in keywords? If enabled, ccscript-mode will automatically modify keywords to their recommended capitalisation while you type them." :group 'ccscript :type 'boolean) (defcustom ccscript-auto-caps-vars t "Automatically fix capitalisation in variable names? If enabled, ccscript-mode will automatically modify variable and proprty names to their recommended capitalisation while you type them. `ccscript-auto-caps' must be set, too, if you enable this" :group 'ccscript :type 'boolean) (defvar ccscript-mode-map nil "Keymap for CCScript mode") (defun ccscript-keyword-regexp () "Return regexp matching keywords Returns a regexp which matches all the keywords from `ccscript-keywords'" (concat "\\<\\(" (mapconcat 'regexp-quote ccscript-keywords "\\|") "\\)\\>") ) (defun ccscript-check-re (RE) "Skip over regexp Like `looking-at', but places point after match if successful" (if (looking-at RE) (goto-char (match-end 0)) nil) ) (defun ccscript-check-paren () "Skip over CCScript parenthesized operand list If looking at a CCScript operand, skip it forward and return t. Otherwise, return nil." (while (or (ccscript-check-expr) (ccscript-check-re "[ \t]*,[ \t]*"))) (ccscript-check-re ")") ) (defun ccscript-check-operand () "Skip over CCScript operand If looking at a CCScript operand, skip it forward and return t. Otherwise, return nil. Very simplified parsing" ;; skip unary operators (ccscript-check-re "\\([- \t+]\\|\\\\)*") (let (ok) ;; identifier or number (if (ccscript-check-re "[a-z0-9._$]+") (setq ok t)) ;; parenthesized expression (if (ccscript-check-re "(") (setq ok (or (ccscript-check-paren) ok))) ;; single-quoted string (if (ccscript-check-re "'[^'\n]*'") (setq ok t)) ;; double-quoted string (if (ccscript-check-re "\"\\([^\"\\\n]*\\|\\[^\n]\\)\"") (setq ok t)) ok) ) (defun ccscript-check-expr () "Skip over CCScript expression If looking at a CCScript expression, skip it forward and return t. Otherwise, return nil." (if (ccscript-check-operand) (progn (while (and (ccscript-check-re "[ \t]*") (ccscript-check-re "[-=.+*/\\&#^;]\\|\\<\\(mod\\|and\\|x?or\\)\\>\\|[<>:][=>]?") (ccscript-check-operand))) t) nil) ) (defun ccscript-classify-current-line (NEW) "Classify current line for indentation of CCScript files Return value is a cons (THIS . NEXT), where THIS is the indentation of this line relative to the previous one, and NEXT is the indentation of the next line relative to this one. Values are multiplied by `ccscript-basic-indent', so sensible values for THIS and NEXT are -1, 0 and 1. Return value may also be NIL to cause this line to be ignored during indentation. NEW says whether this line is the one we're indenting (t) or not (nil)" (let ((case-fold-search t)) (save-excursion (beginning-of-line) (ccscript-check-re "[ \t]+") ; skip leading whitespace (cond ;; Indent line after `Do' or `Sub' ((looking-at "\\<\\(do\\|sub\\)\\>") '(0 . 1)) ;; Outdent `EndWith', `EndSub' etc. ((looking-at "\\<\\(endwith\\|endsub\\|endtry\\|endif\\|next\\|loop\\)\\>") '(-1 . 0)) ;; Indent after `ForEach' or `With' if this is a multiline loop ((and (ccscript-check-re "\\<\\(foreach\\|with\\)\\>") (ccscript-check-expr)) (ccscript-check-re "\\") (if (or (ccscript-check-re "[ \t]*$") (ccscript-check-re "[ \t]*%")) '(0 . 1) '(0 . 0))) ;; Indent *twice* after `Select' ((looking-at "\\") '(0 . 2)) ((looking-at "\\") '(-1 . 1)) ((looking-at "\\") '(0 . -2)) ;; Indent after `If' if this is a multi-line statement ((and (ccscript-check-re "\\<\\(if\\)\\>") (ccscript-check-expr)) (ccscript-check-re "\\") (if (or (ccscript-check-re "[ \t]*$") (ccscript-check-re "[ \t]*%")) '(0 . 1) '(0 . 0))) ;; Indent after multiline `For' ((and (ccscript-check-re "\\<\\(for\\)\\>") (ccscript-check-expr) (ccscript-check-re "\\<\\(to\\)\\>") (ccscript-check-expr)) (ccscript-check-re "\\<\\(do\\)\\>") (if (or (ccscript-check-re "[ \t]*$") (ccscript-check-re "[ \t]*%")) '(0 . 1) '(0 . 0))) ;; Outdent `Else' once, but indent again after ((looking-at "\\") '(-1 . 1)) ;; Indent multi-line `Try' ((ccscript-check-re "\\[ \t]*") (if (looking-at "$\\|%") '(0 . 1) '(0 . 0))) ;; Ignore blank lines ((looking-at "$") (if NEW '(0 . 0) nil)) ;; Ignore comments if at beginning of line; indent them ;; like normal statements otherwise ((looking-at "%") (if (bolp) nil '(0 . 0))) ;; normal command (t '(0 . 0)) ) ) ) ) (defun ccscript-maybe-fix-caps (len) ;; len = length of word to maybe-fix ;; if this word should be fixed, delete it and return new capitalisation ;; if this word needn't be fixed, return "" (let ((case-fold-search t) (words (append ccscript-keywords ccscript-additional-keywords (and ccscript-auto-caps-vars ccscript-variable-keywords))) (return "") word) (while words (setq word (car words) words (cdr words)) (if (and (equal (length word) len) (looking-at (regexp-quote word))) ;; ok, this is our word (progn (setq words nil) (if (equal (buffer-substring (point) (+ (point) len)) word) "" (delete-char len) (setq return word)) ) ) ) return) ) (defun ccscript-in-literal-or-comment () (save-excursion (let ((here (point)) ret chars) (beginning-of-line) (setq chars 1) (while (and (< (point) here) (not (zerop chars))) (setq chars (skip-chars-forward "^%\"'" here)) (setq ret (or (ccscript-check-re "%[^\n]*") (ccscript-check-re "\"\\([^\"\\\n]*\\|\\[^\n]\\)\"?") (ccscript-check-re "'[^\n']'?"))) ) ret ) ) ) (defun ccscript-electric-punct () "CCScript Electric Punctuation When a punctuation key is pressed, case-fix the previous word (if `ccscript-auto-caps' is on), and insert that character." (interactive) (if (and ccscript-auto-caps (not (ccscript-in-literal-or-comment))) (insert (save-excursion (let ((dist (skip-chars-backward "a-zA-Z0-9"))) (if (zerop dist) "" (ccscript-maybe-fix-caps (- dist)))) ) last-command-char) (insert last-command-char) ) ) (defun ccscript-electric-newline () "CCScript electric newline Indent correctly, insert a newline, and place cursor at correct place to continue" (interactive) (let ((last-command-char "\n")) (save-excursion (indent-according-to-mode)) (ccscript-electric-punct) (indent-according-to-mode)) ) (defun ccscript-indent-line (&optional ARG) "Indent line according to CCScript rules" (interactive) (let (thisind thiscode prevind prevcode new) (save-excursion (beginning-of-line) ;; get indentation of this line. If this line doesn't say anything ;; interesting, move up (setq new t) (while (and (not (setq thiscode (ccscript-classify-current-line new))) (equal (forward-line -1) 0)) (setq new nil)) (setq thisind (current-indentation)) ;; get indentation of the line before. (while (and (equal (forward-line -1) 0) (not (setq prevcode (ccscript-classify-current-line nil))))) (if prevcode (setq prevind (current-indentation)) (setq prevind 0 prevcode '(0 . 0))) ) ;; this line must be indented to prevind + cdr prevcode + car thiscode (indent-line-to (max 0 (+ prevind (* ccscript-basic-indent (cdr prevcode)) (* ccscript-basic-indent (or (car thiscode) 0)))) ) ) ) (defun ccscript-mode () "Mode for editing CCScript (.q) files CCScript is the scripting language of Planets Command Center. This mode provides indentation, commenting, electric case-fixing and font-lock patterns for use with such files. Runs `ccscript-mode-hook'. Keys: \\{ccscript-mode-map}" (interactive) (kill-all-local-variables) (if ccscript-mode-map nil (setq ccscript-mode-map (make-sparse-keymap)) (define-key ccscript-mode-map " " 'ccscript-electric-punct) (define-key ccscript-mode-map "(" 'ccscript-electric-punct) (define-key ccscript-mode-map ")" 'ccscript-electric-punct) (define-key ccscript-mode-map "," 'ccscript-electric-punct) (define-key ccscript-mode-map ";" 'ccscript-electric-punct) (define-key ccscript-mode-map "%" 'ccscript-electric-punct) (define-key ccscript-mode-map "-" 'ccscript-electric-punct) (define-key ccscript-mode-map "+" 'ccscript-electric-punct) (define-key ccscript-mode-map "*" 'ccscript-electric-punct) (define-key ccscript-mode-map "/" 'ccscript-electric-punct) (define-key ccscript-mode-map "\\" 'ccscript-electric-punct) (define-key ccscript-mode-map "^" 'ccscript-electric-punct) (define-key ccscript-mode-map "=" 'ccscript-electric-punct) (define-key ccscript-mode-map "<" 'ccscript-electric-punct) (define-key ccscript-mode-map ">" 'ccscript-electric-punct) (define-key ccscript-mode-map "=" 'ccscript-electric-punct) (define-key ccscript-mode-map ":" 'ccscript-electric-punct) (define-key ccscript-mode-map "." 'ccscript-electric-punct) ;; though `$' is a regular name character in CCScript, we ;; treat it as punctuation. This reduces the variable-keywords ;; list by ~15% (define-key ccscript-mode-map "$" 'ccscript-electric-punct) (define-key ccscript-mode-map "\C-m" 'ccscript-electric-newline) (define-key ccscript-mode-map "\C-j" 'ccscript-electric-newline) (define-key ccscript-mode-map "\C-c\C-c" 'comment-region) ) (use-local-map ccscript-mode-map) (setq major-mode 'ccscript-mode mode-name "CCScript" case-fold-search t comment-start "% " comment-end "" comment-start-skip "% *" ccscript-font-lock-keywords (list '("%[^\n]*" 0 font-lock-comment-face t) '("'[^'\n]*'" 0 font-lock-string-face t) '("^sub \\([a-z0-9_.$]*\\)" 1 font-lock-function-name-face) (list (ccscript-keyword-regexp) 0 'font-lock-keyword-face)) indent-line-function 'ccscript-indent-line ) (put 'ccscript-mode 'font-lock-defaults '(ccscript-font-lock-keywords nil t nil beginning-of-line)) (if (fboundp 'decode-coding-region) (let ((mod (buffer-modified-p))) ;; kill ^M's, but set buffer to `not modified' if it was not ;; already modified (decode-coding-region (point-min) (point-max) 'raw-text-dos) (set-buffer-file-coding-system 'raw-text-dos t) (if (not mod) (set-buffer-modified-p nil))) ) (run-hooks 'ccscript-mode-hook) ) (setq auto-mode-alist (cons '("\\.q$" . ccscript-mode) auto-mode-alist)) (provide 'ccscript)