In this post, I’ll describe how to edit Chrome textareas with the Edwin text editor that comes built-in with MIT/GNU Scheme.
If you just want to see the end result, see the screenshot and video at the end of this post.
These instructions will also work with recent releases of the Opera browser (since the newer Chromium-based versions can run Chrome plugins). They may also work at some point with Firefox, when Mozilla implements the new WebExtensions API.
At a high level, the steps to edit Chrome textareas with Edwin are:
- Install a browser add-on
- Customize Edwin with a few hacks
- Write a shell script to make it easy to launch Edwin from the command line
- Run a local “edit server” that interacts with the browser add-on and launches Edwin
On This Page
Install the Edit with Emacs add-on from the Chrome Web Store.
The default way to open Edwin is to run
$ mit-scheme --edit
This just launches an Edwin editor window. From there, you need to manually open files and edit them.
What we need is a way to launch Edwin and open a specific file automatically. Most editors you are familiar with already do this, e.g.,
$ vim /tmp/foo.txt $ emacsclient /tmp/bar.txt
To be able to launch Edwin in this way, we need to hack a few procedures in the file
editor.scm in the MIT/GNU Scheme source and load them from the Edwin init file. We’ll tackle each of these tasks separately below.
To get Edwin to open a file on startup, we need to tweak three procedures in
editor.scm to accept and/or pass around filename arguments:
Here’s the code; you can just paste it into a file somewhere. For the purposes of this post we’ll call it
;;;; open-edwin-on-file.scm -- Rich's hacks to open Edwin on a specific file. ;;; These (minor) changes are all to the file `editor.scm'. They are ;;; all that is needed to allow Edwin to be opened on a specific file ;;; by adding a `filename' argument to the EDIT procedure. (define (create-editor file . args) (let ((args (if (null? args) create-editor-args (begin (set! create-editor-args args) args))) (filename (if (file-exists? file) file #f))) (reset-editor) (event-distributor/invoke! editor-initializations) (set! edwin-editor (make-editor "Edwin" (let ((name (and (not (null? args)) (car args)))) (if name (let ((type (name->display-type name))) (if (not type) (error "Unknown display type name:" name)) (if (not (display-type/available? type)) (error "Requested display type unavailable:" type)) type) (default-display-type '()))) (if (null? args) '() (cdr args)))) (set! edwin-initialization (lambda () (set! edwin-initialization #f) (if filename (standard-editor-initialization filename) (standard-editor-initialization)) (set! edwin-continuation #f) unspecific)))) (define (standard-editor-initialization #!optional filename) (with-editor-interrupts-disabled (lambda () (if (and (not init-file-loaded?) (not inhibit-editor-init-file?)) (begin (let ((filename (os/init-file-name))) (if (file-exists? filename) (load-edwin-file filename '(EDWIN) #t))) (set! init-file-loaded? #t) unspecific)))) (let ((buffer (find-buffer initial-buffer-name)) (filename (if (not (default-object? filename)) ((ref-command find-file) filename) #f))) (if (and buffer (not inhibit-initial-inferior-repl?)) (start-inferior-repl! buffer (nearest-repl/environment) (and (not (ref-variable inhibit-startup-message)) (cmdl-message/append (cmdl-message/active (lambda (port) (identify-world port) (newline port))) (cmdl-message/strings "You are in an interaction window of the Edwin editor." "Type `C-h' for help, or `C-h t' for a tutorial." "`C-h m' will describe some commands." "`C-h' means: hold down the Ctrl key and type `h'."))))))) (define (edit file . args) (call-with-current-continuation (lambda (continuation) (cond (within-editor? (error "edwin: Editor already running")) ((not edwin-editor) (apply create-editor file args)) ((not (null? args)) (error "edwin: Arguments ignored when re-entering editor" args)) (edwin-continuation => (lambda (restart) (set! edwin-continuation #f) (within-continuation restart (lambda () (set! editor-abort continuation) unspecific))))) (fluid-let ((editor-abort continuation) (current-editor edwin-editor) (within-editor? #t) (editor-thread (current-thread)) (editor-thread-root-continuation) (editor-initial-threads '()) (inferior-thread-changes? #f) (inferior-threads '()) (recursive-edit-continuation #f) (recursive-edit-level 0)) (editor-grab-display edwin-editor (lambda (with-editor-ungrabbed operations) (let ((message (cmdl-message/null))) (cmdl/start (make-cmdl (nearest-cmdl) dummy-i/o-port (lambda (cmdl) cmdl ;ignore (bind-condition-handler (list condition-type:error) internal-error-handler (lambda () (call-with-current-continuation (lambda (root-continuation) (set! editor-thread-root-continuation root-continuation) (with-notification-output-port null-output-port (lambda () (do ((thunks (let ((thunks editor-initial-threads)) (set! editor-initial-threads '()) thunks) (cdr thunks))) ((null? thunks)) (create-thread root-continuation (car thunks))) (top-level-command-reader edwin-initialization))))))) message) #f `((START-CHILD ,(editor-start-child-cmdl with-editor-ungrabbed)) (CHILD-PORT ,(editor-child-cmdl-port (nearest-cmdl/port))) ,@operations)) message))))))))
Then, you’ll need to tweak your Edwin init file (also known as
~/.edwin) to load this file into Edwin’s environment on startup:
(load "/path/to/open-edwin-on-file.scm" '(edwin))
Now that the
EDIT procedure takes a filename argument, we can wrap this all up in a shell script that calls Edwin with the right arguments. There may be other ways to accomplish this than in the code shown below, but it works.
Note that the path to my local installation of MIT/GNU Scheme on Mac OS X is slightly tweaked from the official install location. What’s important is that Scheme is invoked using the right “band”, or image file. For more information, see the fine manual.
Take the code below and stick it somewhere on your
$PATH; on my machine it lives at
#!/usr/bin/env sh EDIT_FILE=$1 SCHEME_CODE="(edit \"$EDIT_FILE\")" if [[ $(uname) == 'Darwin' ]]; then _SCHEME_DIR=/Applications/MIT-Scheme.app/Contents/Resources SCHEME=$_SCHEME_DIR/mit-scheme MITSCHEME_BAND=$SCHEME_DIR/all.com CMD=$SCHEME fi if [[ $(uname) == 'Linux' ]]; then CMD=scheme fi N=$RANDOM F=/tmp/edit-$N.scm touch $F echo $SCHEME_CODE > $F $CMD --load $F
Although the extension is called ‘Edit with Emacs’, it can be used with any text editor. You just need to be able to run a local “edit server” that generates the right inputs and outputs. Since Chrome extensions can’t launch apps directly, the extension running in the browser needs to act as a client to a locally running server, which will launch the app.
Since we want to launch Edwin, we’ll need to run a local edit server. Here’s the one that I use:
To get the server to launch Edwin, I save the gist somewhere as
editserver.psgi and run the following script (for more information on the environment variables and what they mean, see the comments in the gist):
#!/usr/bin/env sh EDITSERVER_CMD='edwin %s' \ EDITSERVER_BLOCKING=1 \ screen -d -m `which plackup` -s Starman -p 9292 -a ~/Code/mathoms/editserver.psgi
The relevant bit for running Edwin is the
EDITSERVER_CMD environment variable, which we’ve set to run the
edwin script shown above.
Note that this server is written in Perl and requires you to install the
Plack modules. If you don’t like Perl or don’t know how to install Perl modules, there are other servers out there that should work for you, such as this one written in Python.
Once you’ve done everything above and gotten it working together, you should be able to click the “edit” button next to your browser textarea and start Edwin. It will look something like the following screenshot (which you saw at the beginning of this post):
If you prefer video, check out this short demo on YouTube.