From d3659f6c5606eed5241b43e3ab88b703459173f5 Mon Sep 17 00:00:00 2001 From: John Mastro Date: Wed, 17 Jul 2013 15:58:30 -0700 Subject: Initial commit --- .gitignore | 2 + readme.org | 119 ++++++++++++++++++++++++++++ sps-mode.el | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100644 .gitignore create mode 100644 readme.org create mode 100644 sps-mode.el diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..077ad28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.elc +test diff --git a/readme.org b/readme.org new file mode 100644 index 0000000..9f1fbdb --- /dev/null +++ b/readme.org @@ -0,0 +1,119 @@ +* sps-mode + +This is an [[http://www.gnu.org/software/emacs/][Emacs]] minor mode and collection of commands for working with +[[http://common-lisp.net/project/parenscript/][Parenscript]] code in [[http://common-lisp.net/project/slime/][SLIME]] and sending it to the browser via [[https://github.com/skeeto/skewer-mode][Skewer]]. The goal is +to create an environment for hacking Parenscript which fits as naturally as +possible into the Lisp style of interactive development. + +Note that this is very young code and there are certain to be serious problems. + +There's at least one other project with related goals, [[https://github.com/3b/slime-proxy][slime-proxy]], though at +the time of writing it's unclear whether it's still being actively developed. + +** Installation + +=sps-mode= has both [[http://en.wikipedia.org/wiki/Common_Lisp][Common Lisp]] and Emacs dependencies. + +- Common Lisp dependencies + - Your preferred [[http://en.wikipedia.org/wiki/Common_Lisp][Common Lisp]] implementation + - [[http://common-lisp.net/project/parenscript/][Parenscript]] + +You almost certainly want to use [[http://www.quicklisp.org/beta/][Quicklisp]] to install Parenscript. + +- Emacs dependencies + - [[http://common-lisp.net/project/slime/][SLIME]] + - [[https://github.com/skeeto/skewer-mode][Skewer]] + - [[https://github.com/magnars/s.el][s.el]] + - [[https://github.com/magnars/dash.el][dash.el]] + +My recommendation is to install SLIME via the [[https://github.com/quicklisp/quicklisp-slime-helper][Quicklisp SLIME Helper]] and +the others through [[http://www.emacswiki.org/emacs/ELPA][ELPA]]. + +** Setup + +To enable sps-mode in a SLIME buffer: =M-x sps-mode=. + +To have =lisp-mode=, =slime-mode=, and =sps-mode= all enable automatically for +any file with an extension of ".paren": + +#+BEGIN_SRC emacs-lisp +(add-to-list 'auto-mode-alist (cons "\\.paren\\'" 'lisp-mode)) +(add-hook 'lisp-mode-hook + #'(lambda () + (when (and buffer-file-name + (s-ends-with? ".paren" buffer-file-name)) + (unless (slime-connected-p) + (save-excursion (slime))) + (sps-mode +1)))) +#+END_SRC + +=slime-mode= must be active for both expansion and evaluation commands. For +evaluation commands, =skewer-mode= is also required. (See below for a list of +the available commands). + +Parenscript must be loaded in your Common Lisp image, and you'll probably also +want to import its symbols: + +#+BEGIN_SRC common-lisp +(ql:quickload :parenscript) +(use-package :parenscript) +#+END_SRC + +** Commands + +*** Code expansion commands + +These commands generate JavaScript from the Parenscript code and display it but +don't send it to the browser for evaluation: + + - =sps-expand-sexp= + - =sps-expand-last-expression= + - =sps-expand-defun= + - =sps-expand-region= + - =sps-expand-buffer= + - =sps-expand-dwim= + +From within an expansion buffer you can press "x" to send the JavaScript to the +browser. + +*** Code evaluation commands + +These commands first compile the Parenscript code to JavaScript and then +immediately send to it the browser to be evaluated: + + - =sps-eval-sexp= + - =sps-eval-last-expression= + - =sps-eval-defun= + - =sps-eval-region= + - =sps-eval-buffer= + - =sps-eval-dwim= + +** Keybindings + +Many programming modes, including most Skewer modes, use keybindings which +overlap with SLIME's. To enable those bindings, thus shadowing SLIME's, you can +use =(sps-shadow-slime-keybindings!)=. + +However /this isn't recommended/. There's a good chance you'll want the +flexibility to evaluate the code in an =sps-mode= buffer as either Common Lisp +or Parenscript, in which case you won't want to shadow the SLIME keybindings. +In this case you should assign key bindings yourself. + +** Still do be done + + - Test against a wider array of code. Are there problems with quoting? + - Better documentation. + - Add a REPL and/or a scratch buffer. + - See if more integration with SLIME is possible (e.g. the selector). + - Command(s) for compiling with the output going to a file. + - Similar support for [[http://weitz.de/cl-who/][CL-WHO]] and/or [[https://github.com/paddymul/css-lite][CSS-LITE]]? + - Optional default keybindings that don't shadow SLIME. + - Function(s) to add keybindings behind prefix keys (see js2r). + - Get to know ELPA and packaging. + - Add support for Customize. + +** Contributing + +Contributions are very welcome. Since I've just started working on this and +don't have everything figured out yet, please first contact me on GitHub or +send me an email so we can talk before you start working on something. diff --git a/sps-mode.el b/sps-mode.el new file mode 100644 index 0000000..4ee062a --- /dev/null +++ b/sps-mode.el @@ -0,0 +1,253 @@ +;;; sps-mode.el --- Tools for interactive Parenscript development + +;; Copyright (C) 2013 John Mastro + +;; Author: John Mastro +;; Version: 0.0.1 +;; Keywords: languages, lisp, processes, tools +;; Package-Requires: ((slime "2013-05-26") (skewer-mode "1.5.0") +;; (s "1.6.1") (dash "1.0.3")) + +;;; Commentary: + +;; This is an Emacs minor mode and collection of commands for working with +;; Parenscript code in SLIME and sending it to the browser via Skewer. The goal +;; is to create an environment for hacking Parenscript which fits as naturally +;; as possible into the Lisp style of interactive development. + +;; See https://github.com/johnmastro/sps-mode for additional information. + +;; Note that this is very young code and there are certain to be serious +;; problems. + +;;; Code: + +(require 'dash) +(require 's) + +;;;; Vars + +(defvar sps-mode-map (make-sparse-keymap) + "Keymap for sps-mode. +This keymap is initialized empty. To repurpose the keys SLIME +uses for its normal code evaluation and expansion commands, call +`sps-shadow-slime-keybindings!`. Of course, you can alternatively +define your own keybindings in the usual way.") + +(defvar sps-expansion-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "q") 'quit-window) + (define-key map (kbd "x") 'sps-send-expanded-code) + map) + "Keymap for `sps-expansion-mode` buffers.") + +(defvar sps-expansion-major-mode 'javascript-mode + "The major mode to enable in expansion buffers. +Note that a serious bug seems to be triggered when using js2-mode +in expansion buffers. Avoid it for the time being unless you know +what you're doing." + ;; It would be better to use js2-mode, especially since Skewer requires it + ;; anyway (why make people load two different JavaScript modes)? But, I've + ;; come across a bug somehow related to js2-mode and skewer - when an + ;; expansion is loaded in a js2-mode buffer the code is unexpectedly + ;; evaluated (even though we haven't called skewer-eval) and, worse, Emacs + ;; occasionally freezes. Until I track this down I don't want to leave the + ;; code laying around with js2-mode as the default in case anyone happens + ;; across it and gives it a try. + ) + +;;;; A few utilities + +(defun sps-wrap-in-ps-form (string) + "Return Parenscript STRING wrapped in a PS:PS form." + (format "(ps:ps %s)" string)) + +(defun sps-swank-eval-expr (string) + "Return a form appropriate for evaluating STRING via Swank." + `(swank:eval-and-grab-output ,(sps-wrap-in-ps-form string))) + +(defun sps-file-name-base (filename) + "Return FILENAME's basename without it's extension." + (file-name-sans-extension (file-name-nondirectory filename))) + +;;;; Code expansion + +(defun sps-expand (string) + "Display the JavaScript generated from Parenscript STRING. + +The resulting JavaScript is displayed in a temporary buffer. The +buffer's major mode is determined by the variable +`sps-expansion-major-mode` (`javascript-mode` by default). +`sps-expansion-mode` is enabled as an additional minor mode." + (slime-eval-async (sps-swank-eval-expr string) + #'(lambda (result) + (let ((code (read (cadr result)))) + (slime-with-popup-buffer ("*Parenscript generated JavaScript*") + (setq buffer-read-only nil) + (erase-buffer) + (insert code) + (funcall sps-expansion-major-mode) + (sps-expansion-mode 1) + (font-lock-fontify-buffer) + (goto-char (point-min)) + (setq buffer-read-only t) + (pop-to-buffer (current-buffer))))) + (slime-current-package))) + +;; (defun sps-compile-buffer-to-file (&optional append) +;; (interactive "p") +;; (when (and (buffer-modified-p) +;; (y-or-n-p "Save buffer? ")) +;; (save-buffer)) +;; (let* ((this buffer-file-name) +;; (dir (and this (file-name-directory this))) +;; (initial (and this (concat (sps-file-name-base this) ".js"))) +;; (destination (read-file-name "Destination: " dir nil nil initial nil)) +;; (string (buffer-substring-no-properties (point-min) (point-max)))) +;; (slime-eval-async (sps-swank-eval-expr string) +;; #'(lambda (result) +;; (let ((code (read (cadr result)))) +;; (write-region code nil destination append))) +;; (slime-current-package)))) + +(defun sps-expand-sexp () + "Display the expansion of the form at point." + (interactive) + (sps-expand (slime-sexp-at-point))) + +(defun sps-expand-last-expression () + "Display the expansion of the expression preceding point." + (interactive) + (sps-expand (slime-last-expression))) + +(defun sps-expand-defun () + "Display the expansion of the current toplevel form." + (interactive) + (sps-expand (slime-defun-at-point))) + +(defun sps-expand-region (beg end) + "Display the expansion of the currently active region." + (interactive "r") + (sps-expand (buffer-substring-no-properties beg end))) + +(defun sps-expand-buffer () + "Display the expansion of the current buffer." + (interactive) + (sps-expand-region (point-min) (point-max))) + +(defun sps-expand-dwim () + "Display the expansion of the active region or toplevel form. +If the region is active this is equivalent to invoking +`sps-expand-region`, otherwise it's equivalent to +`sps-expand-defun`." + (interactive) + (if (region-active-p) + (sps-expand-region (region-beginning) (region-end)) + (sps-expand-defun))) + +;;;; Code evaluation + +(defun sps-eval (string) + "Compile Parenscript STRING and evaluate it in the browser. + +The code is first compiled to JavaScript in the CL image and then +sent to the browser via `skewer-eval`." + (slime-eval-async (sps-swank-eval-expr string) + #'(lambda (result) + (let ((code (read (cadr result)))) + (skewer-eval code #'skewer-post-minibuffer))) + (slime-current-package))) + +(defun sps-eval-sexp () + "Evaluate the expression at point as Parenscript." + (interactive) + (sps-eval (slime-sexp-at-point))) + +(defun sps-eval-last-expression () + "Evaluate the expression preceding point as Parenscript." + (interactive) + (sps-eval (slime-last-expression))) + +(defun sps-eval-defun () + "Evaluate the current toplevel form as Parenscript." + (interactive) + (sps-eval (slime-defun-at-point))) + +(defun sps-eval-region (beg end) + "Evaluate the currently active region as Parenscript." + (interactive "r") + (sps-eval (buffer-substring-no-properties beg end))) + +(defun sps-eval-buffer () + "Evaluate the current buffer as Parenscript." + (interactive) + (sps-eval-region (point-min) (point-max))) + +(defun sps-eval-dwim () + "Evaluate the active region or toplevel form. +If the region is active this is equivalent to invoking +`sps-eval-region`, otherwise it's equivalent to +`sps-eval-defun`." + (if (region-active-p) + (sps-eval-region (region-beginning) (region-end)) + (sps-eval-defun))) + +;;;; Keybindings + +(defun sps-shadow-slime-keybindings! () + "Rebind SLIME's evaluation keys for Parenscript. + +This uses the same keys that `slime-mode` uses, thus shadowing +them. If you want to use both sets of evaluation commands in the +same buffer you will likely want to define other keys manually. + +This command must be run in the buffer that the keybindings are +to be used in - it relies on `make-local-variable` to avoid +changing SLIME's bindings in other buffers." + (interactive) + (make-local-variable 'slime-mode-map) + (let ((map slime-mode-map)) + (define-key map (kbd "C-x C-e") nil) + (define-key map (kbd "C-c C-r") nil) + (define-key map (kbd "C-M-x") nil) + (define-key map (kbd "C-c C-k") nil) + (define-key map (kbd "C-c RET") nil)) + (let ((map sps-mode-map)) + (define-key map (kbd "C-x C-e") 'sps-eval-last-expression) + (define-key map (kbd "C-c C-r") 'sps-eval-region) + (define-key map (kbd "C-M-x") 'sps-eval-defun) + (define-key map (kbd "C-c C-k") 'sps-eval-buffer) + (define-key map (kbd "C-c RET") 'sps-expand-sexp))) + +;;;; The minor mode + +;;;###autoload +(define-minor-mode sps-mode + "Minor mode for interactively evaluating Parenscript forms." + :lighter " sps" + :keymap sps-mode-map) + +;;;; Expansion minor mode + +(defun sps-send-expanded-code () + "Send the expanded code to the browser." + (interactive) + (skewer-eval + (buffer-substring-no-properties (point-min) (point-max)) + #'skewer-post-minibuffer)) + +(define-minor-mode sps-expansion-mode + "Minor mode for displaying the code generated by Parenscript." + :lighter nil + :keymap sps-expansion-mode-map) + + +(provide 'sps-mode) + +;; Local Variables: +;; lexical-binding: t +;; indent-tabs-mode: nil +;; coding: utf-8 +;; End: + +;;; sps-mode.el ends here -- cgit v1.2.3