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