Initial import.
[pico8.git] / pico8.el
1 ;;; pico8.el --- major mode for editing PICO-8 cartridges
2 ;;
3 ;; Author: Joe Wreschnig <joe.wreschnig@gmail.com>
4 ;; Package-Version: 20170620
5 ;; Package-Requires: ((emacs "25") (polymode "20170307") (lua-mode "20151025"))
6 ;; Keywords: convenience
7 ;;
8 ;; This program is free software; you can redistribute it and/or
9 ;; modify it under the terms of the GNU General Public License
10 ;; as published by the Free Software Foundation; either version 3
11 ;; of the License, or (at your option) any later version.
12
13 ;;; Commentary:
14 ;;
15 ;; This mode (ab)uses polymode to fit six modes into one buffer, one
16 ;; of which is "real Lua" text and the other five of which have
17 ;; diverse strict formatting requirements.
18 ;;
19 ;; It provides the keybinds and commands for inter-mode actions.
20
21 ;;; Code:
22
23 (require 'polymode)
24 (require 'lua-mode)
25 (require 'thingatpt)
26
27
28 (defgroup pico8 nil
29 "Support for PICO-8 (.p8) cartridge files."
30 :tag "PICO-8"
31 :group 'languages)
32
33 (defgroup pico8-faces nil
34 "Faces for PICO-8 (.p8) cartridge files."
35 :tag "PICO-8 Faces"
36 :group 'pico8)
37
38 (defcustom pico8-executable-paths
39 '("/Applications/PICO-8.app/Contents/MacOS/pico8" ; macOS
40 "/usr/lib/pico8/pico8" ; PocketCHIP
41 "pico8") ; Normal systems
42 "The locations to search for the PICO-8 executable."
43 :group 'pico8
44 :tag "PICO-8 Executable Paths"
45 :type '(repeat string))
46
47 (defcustom pico8-preserve-output-on-exit nil
48 "Whether to keep the output buffer when PICO-8 exits.
49
50 PICO-8 processes are long-lived with little surprising output, so
51 their output buffers are killed by default when they exit.
52 However, this is not usual behavior in Emacs, and can be
53 disabled by setting this to t."
54 :group 'pico8
55 :tag "Preserve PICO-8 Output On Exit"
56 :type 'boolean)
57
58 (defcustom pico8-lua-indent-level 1
59 "Default indentation for PICO-8 Lua mode.
60
61 This overrides lua-indent-mode in `pico8-lua-mode'. `lua-mode''s
62 default indentation is 3, which is both idiosyncractic and quite
63 large when viewed in the PICO-8 editor, where the convetion is 1."
64 :group 'pico8
65 :tag "PICO-8 Lua Indent Level")
66
67 (defconst pico8-colors
68 (mapcar #'symbol-value
69 (list (defconst pico8-color-0 "#000000")
70 (defconst pico8-color-1 "#1D2B53")
71 (defconst pico8-color-2 "#7E2553")
72 (defconst pico8-color-3 "#008751")
73 (defconst pico8-color-4 "#AB5236")
74 (defconst pico8-color-5 "#5F574F")
75 (defconst pico8-color-6 "#C2C3C7")
76 (defconst pico8-color-7 "#FFF1E8")
77 (defconst pico8-color-8 "#FF004D")
78 (defconst pico8-color-9 "#FFA300")
79 (defconst pico8-color-a "#FFEC27")
80 (defconst pico8-color-b "#00E436")
81 (defconst pico8-color-c "#29ADFF")
82 (defconst pico8-color-d "#83769C")
83 (defconst pico8-color-e "#FF77A8")
84 (defconst pico8-color-f "#FFCCAA"))))
85
86 (defconst pico8-data-characters
87 (append "0123456789abcdef" nil))
88
89 (defface pico8-data-face
90 '((t (:inherit fixed-pitch)))
91 "Face for PICO-8 binary data."
92 :group 'pico8-faces)
93
94 (define-derived-mode pico8-data-mode fundamental-mode
95 "PICO-8 Data"
96 "Major mode for non-text PICO-8 cartridge sections.
97
98 This is an 'abstract' mode and should not be used
99 as an actual major mode, only to derive new modes.")
100
101 (defun pico8-data-self-insert-command (n)
102 "Insert the data character you type N times.
103
104 PICO-8 cartridges represent binary data using fixed-length
105 strings of 0-f, one character per nybble. This command will only
106 insert the typed character if it is one of these characters,
107 overwriting one of these characters."
108 (interactive "P")
109 (when (memq (char-after) pico8-data-characters)
110 (let ((overwrite-mode t))
111 (self-insert-command (prefix-numeric-value n)))))
112
113 (let ((map pico8-data-mode-map))
114 (suppress-keymap map)
115 (dolist (c pico8-data-characters)
116 (define-key map (format "%c" c) 'pico8-data-self-insert-command)))
117
118 (defun pico8-goto-char (position)
119 "Set point to POSITION, a number.
120
121 The position is global to the cartridge, and the buffer is
122 widened if necessary to reach it."
123 (unless (<= (point-min) position (point-max))
124 (widen))
125 (goto-char position))
126 \f
127 (defun pico8-executable ()
128 "Look up the PICO-8 executable."
129 (or (car (delete nil (mapcar 'executable-find pico8-executable-paths)))
130 (error "The PICO-8 executable could not be found.
131 Make sure it is installed, and present in `pico8-executable-paths'")))
132
133 (defun pico8--create-output-buffer ()
134 "Create and return a buffer for PICO-8 output."
135 (let* ((output-name (generate-new-buffer-name "*PICO-8*"))
136 (output (get-buffer-create output-name)))
137 (display-buffer output)
138 (with-current-buffer output
139 (insert "(Use C-r within PICO-8 to reload changes from Emacs.)\n")
140 ;; Setting the point in the buffer doesn't have a lasting
141 ;; effect, we need to change it in the window it opened in.
142 ;; https://emacs.stackexchange.com/questions/21464/
143 (set-window-point (get-buffer-window output) (point-max))
144 (compilation-mode))
145 output))
146
147 (defun pico8--process-sentinel (process signal)
148 "Delete buffers and windows for PROCESS if SIGNAL is exit."
149 (when (and (not pico8-preserve-output-on-exit)
150 (process-buffer process)
151 (memq (process-status process) '(exit signal)))
152 (let ((buffer (process-buffer process)))
153 (dolist (window (get-buffer-window-list buffer))
154 (quit-restore-window window))
155 (kill-buffer buffer))))
156
157 (defun pico8--execute (&rest params)
158 "Run PICO-8 with the provided PARAMS after saving etc."
159 (let ((pico8 (pico8-executable))
160 (cartridge-file-name (buffer-file-name (buffer-base-buffer))))
161 (when (and (buffer-modified-p)
162 (y-or-n-p (format "Save %s? " cartridge-file-name)))
163 (save-buffer))
164 (let* ((output (pico8--create-output-buffer))
165 (process-args (append params (list cartridge-file-name)))
166 (process (apply #'start-file-process "PICO-8" output pico8
167 process-args)))
168 (set-process-sentinel process 'pico8--process-sentinel))))
169
170 (defun pico8-run-cartridge ()
171 "Run the current PICO-8 cartridge."
172 (interactive)
173 (pico8--execute "-run"))
174
175 (defun pico8-load-cartridge ()
176 "Load the current cartridge in PICO-8."
177 (interactive)
178 (pico8--execute))
179
180
181 (defun pico8-cartridge-section-header (name)
182 "Return the header string for cartridge section NAME."
183 (format "__%s__" name))
184
185 (defconst pico8-cartridge-sections
186 '("lua" "gfx" "gff" "map" "sfx" "music"))
187
188 (defconst pico8-cartridge-keywords
189 (mapcar #'pico8-cartridge-section-header pico8-cartridge-sections))
190
191 (defconst pico8-cartridge-header
192 "pico-8 cartridge // http://www.pico-8.com\nversion [0-9]+")
193
194 (define-derived-mode pico8-cartridge-mode fundamental-mode
195 "Cartridge"
196 "Major mode for showing PICO-8 cartridge structure."
197 (font-lock-add-keywords
198 nil (list (regexp-opt pico8-cartridge-keywords 'symbols)
199 (cons pico8-cartridge-header font-lock-comment-face)))
200 (suppress-keymap pico8-cartridge-mode-map))
201
202 (defun pico8-cartridge-point-of-section (name)
203 "Find the point where the data for section NAME begins."
204 (save-restriction
205 (widen)
206 (save-excursion
207 (goto-char (point-min))
208 (let ((token (format "^%s\n" (pico8-cartridge-section-header name))))
209 (if (not (or (re-search-forward token nil t)
210 (re-search-forward token nil t)))
211 (error "Unable to find a %s section in current buffer" name)))
212 (point))))
213
214 (defun pico8-cartridge-goto-section (name)
215 "Go to the beginning of section NAME."
216 (interactive "sGoto PICO-8 cartridge section: ")
217 (pico8-goto-char (pico8-cartridge-point-of-section name)))
218
219 (defmacro pico8-cartridge-with-section (section &rest body)
220 "Go to and narrow SECTION and evaluate BODY there."
221 `(save-restriction
222 (save-excursion
223 (pico8-cartridge-goto-section ,section)
224 (pm-narrow-to-span)
225 ,@body)))
226 \f
227 (defconst pico8-lua-builtins
228 (regexp-opt
229 '("abs" "add" "all" "atan2" "btn" "btnp" "camera"
230 "cartdata" "circ" "circfill" "clip" "cls" "cocreate"
231 "coresume" "costatus" "color" "cos" "cstore" "cursor"
232 "del" "dget" "dset" "fget" "flip" "flr" "folder" "foreach"
233 "fset" "info" "line" "load" "ls" "map" "max" "memcpy"
234 "memset" "menuitem" "mget" "mid" "min" "mset" "music"
235 "pairs" "pal" "palt" "peek" "pget" "poke" "print"
236 "printh" "pset" "reboot" "rect" "rectfill" "reload"
237 "resume" "rnd" "run" "save" "sfx" "sget" "sin" "spr"
238 "sqrt" "srand" "sset" "sspr" "stat") 'symbols))
239
240 (define-derived-mode pico8-lua-mode lua-mode
241 "Lua"
242 "Major mode for editing Lua code in PICO-8 cartridges."
243 (font-lock-add-keywords
244 nil `((,pico8-lua-builtins . font-lock-builtin-face)))
245 (set (make-local-variable 'lua-indent-level) pico8-lua-indent-level))
246
247 \f
248 (defun pico8-gff-current-position ()
249 "Calculate the flag position of the cursor."
250 (pm-with-narrowed-to-span (pm-get-innermost-span)
251 (let ((row (1- (line-number-at-pos)))
252 (col (min 255 (current-column))))
253 (+ (/ col 2) (* row 128)))))
254
255 (defun pico8-gff-lighter ()
256 "Calculate the flag under the cursor."
257 (pm-with-narrowed-to-span (pm-get-innermost-span)
258 (let ((row (1- (line-number-at-pos)))
259 (col (current-column)))
260 (+ (* 128 row) (/ col 2)))))
261
262 (define-derived-mode pico8-gff-mode pico8-data-mode
263 '(:eval (format "Flag[%d]" (pico8-gff-lighter)))
264 "Major mode for editing flags in PICO-8 cartridges.")
265
266 (defun pico8-gff-offset-of-flag (flag)
267 "Calculate the offset of of flag number FLAG."
268 (unless (<= 0 flag 255)
269 (error "Valid flag numbers are 0 to 255, inclusive"))
270 (+ (* 2 flag) (if (> flag 128) 1 0)))
271 \f
272 (defgroup pico8-pixel-faces nil
273 "Font faces to use for PICO-8 pixels.
274
275 Rather than customizing each directly, you'll probably just want
276 to change `pico8-pixel-face'."
277 :tag "PICO-8 Pixel Faces"
278 :group 'pico8-faces)
279
280 (defface pico8-pixel-face
281 '((t (:inherit pico8-data-face :height 100)))
282 "Face for PICO-8 sprite 'pixels'."
283 :group 'pico8-faces)
284
285 (dotimes (i (length pico8-colors))
286 (let ((c (nth i pico8-colors)))
287 (eval `(defface ,(intern (format "pico8-pixel-%x" i))
288 '((t (:inherit pico8-pixel-face :foreground ,c)))
289 ,(format "Face for PICO-8 sprite 'pixel' %x" i)
290 :group 'pico8-pixel-faces
291 :tag ,(format "Face for PICO-8 sprite 'pixel' %x." i)))))
292
293 (defconst pico8-gfx-font-lock-keywords
294 `(("0+" . 'pico8-pixel-0)
295 ("1+" . 'pico8-pixel-1)
296 ("2+" . 'pico8-pixel-2)
297 ("3+" . 'pico8-pixel-3)
298 ("4+" . 'pico8-pixel-4)
299 ("5+" . 'pico8-pixel-5)
300 ("6+" . 'pico8-pixel-6)
301 ("7+" . 'pico8-pixel-7)
302 ("8+" . 'pico8-pixel-8)
303 ("9+" . 'pico8-pixel-9)
304 ("a+" . 'pico8-pixel-a)
305 ("b+" . 'pico8-pixel-b)
306 ("c+" . 'pico8-pixel-c)
307 ("d+" . 'pico8-pixel-d)
308 ("e+" . 'pico8-pixel-e)
309 ("f+" . 'pico8-pixel-f)
310
311 ;; If the \n isn't in the smaller face the line is taller to
312 ;; accomodate the full sized point at the end-of-line.
313 ("\n" . 'pico8-pixel-0)))
314
315 (defun pico8-gfx-current-position ()
316 "Calculate the sprite and in-sprite position of the cursor."
317 ;; FIXME: Ensure the span we got was actually the gfx one.
318 (pm-with-narrowed-to-span (pm-get-innermost-span)
319 (let ((row (1- (line-number-at-pos)))
320 (col (min 127 (current-column))))
321 (list (+ (* 16 (/ row 8)) (/ col 8))
322 (% col 8) (% row 8)))))
323
324 (defun pico8-forward-sprite (n)
325 "Move the point N sprites forward (backward if N is negative)."
326 (interactive "P")
327 (let* ((n (prefix-numeric-value n))
328 (current (pico8-gfx-current-position))
329 (offset (pico8-gfx-offset-of-sprite
330 (% (+ 256 n (nth 0 current)) 256)
331 (nth 1 current)
332 (nth 2 current))))
333 (pm-with-narrowed-to-span (pm-get-innermost-span)
334 (goto-char (+ (point-min) offset)))))
335
336 (defun pico8-backward-sprite (n)
337 "Move the point N sprites backward (forward if N is negative)."
338 (interactive "P")
339 (pico8-forward-sprite (- (prefix-numeric-value n))))
340
341 (defun pico8-gfx-lighter ()
342 "Show a short description of the current sprite position."
343 (let ((current (pico8-gfx-current-position)))
344 (if current (apply #'format (cons "Sprite[%d:%d,%d]" current))
345 "Sprite[-]")))
346
347 (define-derived-mode pico8-gfx-mode pico8-data-mode
348 '(:eval (pico8-gfx-lighter))
349 "Major mode for editing sprites in PICO-8 cartridges."
350 (font-lock-add-keywords nil pico8-gfx-font-lock-keywords)
351 (read-only-mode t))
352
353 (defun pico8-gfx-offset-of-sprite (sprite &optional x y)
354 "Calculate the point of SPRITE's X,Y pixel (0,0 by default)."
355 (let ((x (or x 0))
356 (y (or y 0))
357 (line (* (/ sprite 16) 8))
358 (row (* (% sprite 16) 8)))
359 ;; A limit of 4x4 is kind of arbitrary but if you're using sprites
360 ;; larger than that you probably aren't going to be doing so in a
361 ;; way that this command is useful anyway.
362 (unless (and (<= 0 x 31) (<= 0 y 31))
363 (error "Valid sprite offsets are 0 to 31, inclusive"))
364 (unless (<= 0 sprite 255)
365 (error "Valid sprite numbers are 0 to 255, inclusive"))
366 (+ (* 129 (+ y line)) row x)))
367
368 (define-key pico8-gfx-mode-map "q" 'pico8-backward-sprite)
369 (define-key pico8-gfx-mode-map "w" 'pico8-forward-sprite)
370
371 (defface pico8-map-tile-face
372 '((t (:inherit pico8-data-face :height 100)))
373 "Face for PICO-8 map 'tiles'."
374 :group 'pico8-faces)
375
376 (defconst pico8-map-font-lock-keywords
377 '(("[0-9a-f]+" . 'pico8-map-tile-face)
378 ("\n" . 'pico8-map-tile-face)))
379
380 (defun pico8-map-lighter ()
381 "Calculate the map tile under the cursor."
382 (pm-with-narrowed-to-span (pm-get-innermost-span)
383 (let ((row (- (line-number-at-pos) 1))
384 (col (current-column)))
385 ;; TODO: Show sprite number and flags value
386 (format "%d,%d" (/ col 2) row))))
387
388 (define-derived-mode pico8-map-mode pico8-data-mode
389 '(:eval (format "Map[%s]" (pico8-map-lighter)))
390 "Major mode for editing map data in PICO-8 cartridges."
391 (setq font-lock-defaults '(pico8-map-font-lock-keywords)))
392 \f
393 (defun pico8-sfx-lighter ()
394 "Calculate the sound effect under the cursor."
395 (pm-with-narrowed-to-span (pm-get-innermost-span)
396 (let ((row (- (line-number-at-pos) 1)))
397 (format "%d" row))))
398
399 (define-derived-mode pico8-sfx-mode pico8-data-mode
400 '(:eval (format "Sound[%s]" (pico8-sfx-lighter)))
401 "Major mode for editing sound data in PICO-8 cartridges.")
402 \f
403 (defun pico8-music-lighter ()
404 "Calculate the map tile under the cursor."
405 (pm-with-narrowed-to-span (pm-get-innermost-span)
406 (let ((row (- (line-number-at-pos) 1)))
407 (format "%d" row))))
408
409 (define-derived-mode pico8-music-mode pico8-data-mode
410 '(:eval (format "Pattern[%s]" (pico8-music-lighter)))
411 "Major mode for editing music data in PICO-8 cartridges.")
412 \f
413 (defun pico8-goto-sprite (sprite &optional x y)
414 "Set point to the top-left pixel of SPRITE (or the X,Y pixel)."
415 (interactive "nGo to sprite [0-255]: ")
416 (let ((base (pico8-cartridge-point-of-section "gfx"))
417 (offset (pico8-gfx-offset-of-sprite sprite x y)))
418 (pico8-goto-char (+ base offset))))
419
420 (defun pico8-goto-flag (flag)
421 "Set point to the start of flag number FLAG."
422 (interactive "nGo to flag [0-255]: ")
423 (let ((base (pico8-cartridge-point-of-section "gff"))
424 (offset (pico8-gff-offset-of-flag flag)))
425 (pico8-goto-char (+ base offset))))
426
427 (defun pico8--string-to-number (string)
428 "Convert STRING to a number, guessing the base.
429
430 Returns nil, not 0, if the string was not converted."
431 (cond ((string-match-p "^0[xX][0-9a-fA-F]+$" string)
432 (string-to-number (substring string 2) 16))
433 ((string-match-p "^0[0-9]+$" string)
434 (string-to-number (substring string 1) 8))
435 ((string-match-p "^[0-9]+$" string)
436 (string-to-number string 10))
437 (t nil)))
438
439 (defun pico8-sprite-relevant-to-point ()
440 "Get the sprite number relevant to the point.
441
442 When editing a flag, this is the flag number. When editing a
443 map, this is the value at the map. When editing Lua code,
444 this is the numeric literal in the code."
445 (cond
446 ((derived-mode-p 'pico8-gff-mode) (pico8-gff-current-position))
447
448 ;; The sprite for a map is the data at the map location, which is
449 ;; to say, the hexadecimal interpretation of the two character
450 ;; string beginning at the previous even column.
451 ((derived-mode-p 'pico8-map-mode)
452 (let ((beg (- (point) (% (min 255 (current-column)) 2))))
453 (string-to-number (buffer-substring beg (+ beg 2)) 16)))
454
455 ;; In Lua or other code, the sprite is a numeric literal.
456 ;; lua-mode doesn't derive from anything.
457 ((or (derived-mode-p 'lua-mode) (derived-mode-p 'prog-mode))
458 (pico8--string-to-number (or (word-at-point) "")))
459
460 (t nil)))
461
462
463 (defun pico8-goto-sprite-relevant-to-point ()
464 "Go to the sprite number relevant to the text at the point.
465
466 When editing a flag, this is the flag number. When editing a
467 map, this is the value at the map. When editing Lua code,
468 this is the numeric literal in the code."
469 (interactive)
470 (let ((sprite (pico8-sprite-relevant-to-point)))
471 (if sprite (pico8-goto-sprite sprite)
472 (error "No sprite number was found at the point"))))
473
474
475 (defun pico8-goto-thing-relevant-to-point ()
476 "Go to the thing relevant to the text at the point.
477
478 When editing a flag or map, this is the corresponding sprite.
479 When editing a sprite, this is the corresponding flag. When
480 editing Lua code, how lucky are you feeling?
481
482 This function needs a lot of work.."
483 (interactive)
484 (cond ((derived-mode-p 'pico8-gfx-mode)
485 (pico8-goto-flag (car (pico8-gfx-current-position))))
486
487 ((or (derived-mode-p 'pico8-gff-mode)
488 (derived-mode-p 'pico8-map-mode))
489 (pico8-goto-sprite-relevant-to-point))
490
491 ((derived-mode-p 'lua-mode)
492 (let ((n (or (pico8--string-to-number (word-at-point)) 0))
493 (c (save-excursion
494 (and (re-search-backward "\\<f[gs]et\\|s?spr\\>"
495 (- (point) 30) t)
496 (char-after)))))
497 ;; FIXME: Actually parse something? lua-mode's sexp
498 ;; commands don't seem too good, and counting parentheses
499 ;; by hand is for nerds.
500 (cond ((= c ?f) (pico8-goto-flag n))
501 ((= c ?s) (pico8-goto-sprite n))
502 (t (error "There's nothing obvious to go to")))))
503
504 (t (error "There's no obvious thing to go to"))))
505 \f
506 ;;;
507 ;; Flycheck Integration
508 ;;
509 ;; This is more or less the same as the default Lua checkers, but we
510 ;; need to write out a temporary file so it doesn't check the non-Lua
511 ;; parts of the file.
512
513 (defun pico8--lua-only (f &rest args)
514 "If this is a PICO-8 buffer, run F(ARGS) on only the current section."
515 (if (derived-mode-p 'pico8-lua-mode)
516 (pico8-cartridge-with-section "lua"
517 (let ((mainbuf (current-buffer))
518 (from (point-min))
519 (to (point-max)))
520 (with-temp-buffer
521 (insert "\n\n\n") ;; match line number with stripped header
522 (insert-buffer-substring mainbuf from to)
523 (insert " -- start __gfx__") ;; otherwise last line is ignored
524 (apply f args))))
525 (apply f args)))
526
527 (defconst pico8--lua-luacheckrc
528 (expand-file-name
529 "pico8.luacheckrc"
530 (file-name-directory (or load-file-name buffer-file-name))))
531
532 (eval-when-compile
533 (require 'flycheck))
534
535 (with-eval-after-load 'flycheck
536 (advice-add 'flycheck-save-buffer-to-file :around #'pico8--lua-only)
537
538 (flycheck-define-checker pico8-lua
539 "A PICO-8 Lua syntax checker using the Lua compiler.
540 See URL `http://www.lua.org/'."
541 :command ("luac" "-p" source)
542 :standard-input nil
543 :error-patterns
544 ((error line-start
545 ;; Skip the name of the luac executable.
546 (minimal-match (zero-or-more not-newline))
547 ":" line ": " (message) line-end))
548 :modes pico8-lua-mode)
549
550 (flycheck-define-checker pico8-luacheck
551 "A PICO-8 Lua syntax checker using luacheck.
552 See URL `https://github.com/mpeterv/luacheck'."
553 :command ("luacheck"
554 "--formatter" "plain"
555 "--codes" ; Show warning codes
556 "--no-color"
557 (option "--config" pico8--lua-luacheckrc)
558 "--filename" source-original
559 source)
560 :error-patterns
561 ((warning line-start
562 (optional (file-name))
563 ":" line ":" column
564 ": (" (id "W" (one-or-more digit)) ") "
565 (message) line-end)
566 (error line-start
567 (optional (file-name))
568 ":" line ":" column ":"
569 ;; `luacheck' before 0.11.0 did not output codes for errors, hence
570 ;; the ID is optional here
571 (optional " (" (id "E" (one-or-more digit)) ") ")
572 (message) line-end))
573 :modes pico8-lua-mode)
574
575 (add-to-list 'flycheck-checkers 'pico8-lua)
576 (add-to-list 'flycheck-checkers 'pico8-luacheck))
577
578
579 ;;;
580 ;; Finally - pico8-mode!
581
582 (defmacro pico8--defchunkmode (name)
583 "Define a PICO-8 polymode chunk for section NAME."
584 `(defconst ,(intern (concat "pico8--pm-inner-" name))
585 (pm-hbtchunkmode :mode ',(intern (format "pico8-%s-mode" name))
586 :head-mode 'host
587 :head-reg ,(format "^__%s__\n" name)
588 :tail-reg "^__[a-z]\\{3,5\\}__\n\\|^\n\\'")))
589
590 (defconst pico8--pm-poly
591 (pm-polymode-multi
592 :hostmode
593 (defconst pico8--pm-host
594 (pm-bchunkmode :mode 'pico8-cartridge-mode))
595 :innermodes (list
596 (pico8--defchunkmode "lua")
597 (pico8--defchunkmode "gfx")
598 (pico8--defchunkmode "gff")
599 (pico8--defchunkmode "map")
600 (pico8--defchunkmode "sfx")
601 (pico8--defchunkmode "music"))))
602
603 (define-polymode pico8-mode pico8--pm-poly
604 :lighter "P8"
605 :keymap '(("\C-c\C-r" . pico8-run-cartridge)
606 ("\C-c\C-e" . pico8-load-cartridge)
607 ("\M-gs" . pico8-goto-sprite)
608 ("\M-gS" . pico8-goto-sprite-relevant-to-point)
609 ("\M-gf" . pico8-goto-flag)
610 ("\M-g." . pico8-goto-thing-relevant-to-point))
611 (toggle-truncate-lines 1))
612
613 (add-to-list 'auto-mode-alist '("\\.p8$" . pico8-mode))
614 (add-to-list 'magic-mode-alist '("pico-8 cartridge" . pico8-mode))
615
616
617 (provide 'pico8)
618 ;;; pico8.el ends here