I write Common Lisp in VS Code, and out of the box that means syntax colors and a terminal. Everything that makes Lisp pleasant to work in, evaluating a form where it sits, asking the running image what a function takes, picking a restart when something breaks, lived in Emacs with SLIME. I wanted it in the editor I already use.

So I built moro, a Common Lisp extension for VS Code. There is nothing to install on the Lisp side: the language server is bundled with the extension, and the REPL backend is one dependency-free Lisp file that moro loads into whatever implementation you run. It is Apache 2.0.

What You Get Without a REPL

  • Highlighting that follows the Lisp reader rather than the line. A head or a definer name that starts on the next line is still colored, #+(and sbcl unix) feature expressions get their own scopes, and |a symbol with a newline| stays one symbol.
  • Indentation the way SLIME does it. The LOOP rules are a port of SLIME’s common-lisp-indent-loop-macro-1 (#14), and a #+sbcl alone on its line is indented as the argument it guards.
  • Navigation that knows packages. Go to definition, references, rename and highlights resolve every symbol to its home package from the file’s in-package and your defpackage forms, so app::run and other::run are different symbols and renaming one leaves the other alone (#19).
  • Your dependencies, indexed. The server reads your .asd files and ql:quickload calls, resolves the systems through ocicl, Quicklisp, qlot, Roswell, ~/common-lisp and the ASDF source registry, and indexes their sources read-only. Their exported symbols complete, every definition has hover and signature help, go to definition opens the library source, and rename never edits it.
  • Hover and signature help with the lambda list and docstring, for your code and for every standard symbol, with a link to the HyperSpec page.

What You Get With One

Ctrl+Alt+R starts your implementation in a terminal. SBCL, Roswell, Clozure CL, ECL, CLISP and ABCL are detected, and anything else goes in a setting. When the implementation has threads, the backend comes along: a thread listening on a loopback port, authenticated by a token file only your user can read, reading requests with *read-eval* bound to nil. From then on:

  • Completion, hover and go to definition for whatever is loaded, in any package, whenever the static index has no answer. Definition locations come from the image on SBCL, Clozure CL, ECL and ABCL, and each has its own idea of where a form is: SBCL and ECL report the position right after the previous form, ABCL one character later, CCL the form’s own start (#32).
  • Evaluation in place. Ctrl+Enter evaluates the form at the cursor in the image. The values appear after the form and in a results channel, output and warnings are captured, and Interrupt Evaluation stops a form that will not finish.
  • A debugger with restarts. An error opens a panel with the condition, the restarts the evaluation offers and the backtrace. Number keys choose a restart, use-value and store-value ask for a form, frames expand to their locals on SBCL, and the condition or any local opens in the inspector. Closing the panel abandons the evaluation (#31).
  • An inspector on the value of any form: slots, elements, hash table entries, a symbol’s cells, each drillable. Macroexpand opens the expansion as a Lisp document beside the editor; describe and apropos answer in the results channel.
  • Systems from anywhere. Load ASDF System lists the systems defined in the workspace and those in moro.systems, a list of .asd files or directories holding them, written absolute, with ~, or relative to a workspace folder. The picker also offers to browse for an .asd anywhere on disk, and the choice is written to the workspace settings, as a relative path such as ../lib/lib.asd when the two folders are siblings, so it travels with the project. Before a load, moro requires ASDF, puts the directories of every known system on asdf:*central-registry* with the chosen one first, and only then calls load-system, so systems that depend on each other resolve with no source-registry setup, and an image started without an init file works. The language server indexes those systems like dependencies, and moro.repl.loadOnStart loads the ones you name once the backend has connected (#36).

What Is Checked

The protocol is tested against real implementations, not mocks. The suite is 238 node tests, with the backend run on SBCL, ECL, Clozure CL and ABCL for the connection, arglists, evaluation, the debugger loop, the inspector and definition sources, plus 10 tests that start a real VS Code window with the extension and drive it through commands, a fake REPL and an SBCL REPL (#36, #20). CI runs the suite on Linux, macOS and Windows, with the window tests under xvfb on Linux.

The limits I know of: CLISP has no threads, so it gets the terminal REPL only. Backtraces come from SBCL and Clozure CL; on ECL and ABCL the debugger panel shows the condition and the restarts without frames. Frame locals are SBCL’s, and complete only at (debug 3).

Install

Search for moro in the Extensions view, or install juliojimenez.moro from the Marketplace. It is young, at 0.4.1, so expect rough edges and file them. If you write Common Lisp and would rather not run Emacs to get a live image, give moro a try.