codemirror
The CodeMirror 6 editor, packaged as Rails assets.
Point being: you can drop a real code editor into a Rails app without adding
Node, a bundler, or a build step. Install the gem, import from "codemirror"
in your JavaScript, done. It's built for the Propshaft + import map setup that
ships with Rails 7.1 and 8.
The old codemirror-rails gem stopped at CodeMirror 5 years ago. This is a
fresh start for CodeMirror 6.
Install
# Gemfile
gem "codemirror"
bundle install
Requires Ruby 4.0+. That's the whole setup. The gem registers its assets with the pipeline and adds its pins to your import map automatically, so there's nothing to configure. If you want to double-check the pins landed:
bin/importmap json | grep codemirror
You'll want propshaft and importmap-rails, which are already there in a
default Rails app.
Usage
Import from the usual CodeMirror package names, same as you would with npm:
// app/javascript/editor.js
import { EditorView, basicSetup } from "codemirror"
import { javascript } from "@codemirror/lang-javascript"
import { oneDark } from "@codemirror/theme-one-dark"
export function mountEditor(parent, doc = "") {
return new EditorView({
doc,
parent,
extensions: [basicSetup, javascript(), oneDark],
})
}
Most of the time you'll want this behind a Stimulus controller, syncing the editor into a hidden field so the form actually submits the code:
// app/javascript/controllers/code_editor_controller.js
import { Controller } from "@hotwired/stimulus"
import { EditorView, basicSetup } from "codemirror"
import { EditorState } from "@codemirror/state"
import { javascript } from "@codemirror/lang-javascript"
import { oneDark } from "@codemirror/theme-one-dark"
export default class extends Controller {
static targets = ["input"]
connect() {
this.view = new EditorView({
state: EditorState.create({
doc: this.inputTarget.value,
extensions: [
basicSetup,
javascript(),
oneDark,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
this.inputTarget.value = update.state.doc.toString()
}
}),
],
}),
parent: this.element,
})
}
disconnect() {
this.view?.destroy()
}
}
<%# app/views/snippets/_form.html.erb %>
<div data-controller="code-editor">
<%= form.hidden_field :body, data: { code_editor_target: "input" } %>
</div>
What's in the box
The core codemirror package (basicSetup, minimalSetup, EditorView), the
lower-level pieces it's built on (@codemirror/state, view, language,
commands, search, autocomplete, lint), the One Dark theme, and a bunch
of languages:
import { javascript } from "@codemirror/lang-javascript" // + TypeScript, JSX
import { html } from "@codemirror/lang-html"
import { css } from "@codemirror/lang-css"
import { json } from "@codemirror/lang-json"
import { python } from "@codemirror/lang-python"
import { markdown } from "@codemirror/lang-markdown"
import { sql } from "@codemirror/lang-sql"
import { xml } from "@codemirror/lang-xml"
import { rust } from "@codemirror/lang-rust"
import { cpp } from "@codemirror/lang-cpp"
import { java } from "@codemirror/lang-java"
import { php } from "@codemirror/lang-php"
import { yaml } from "@codemirror/lang-yaml"
import { go } from "@codemirror/lang-go"
import { vue } from "@codemirror/lang-vue"
import { angular } from "@codemirror/lang-angular"
import { sass } from "@codemirror/lang-sass"
import { less } from "@codemirror/lang-less"
import { wast } from "@codemirror/lang-wast"
import { liquid } from "@codemirror/lang-liquid"
Every package is pinned individually, so nothing loads until you import it. Need a keymap or a specific extension? Those specifiers work too:
import { keymap } from "@codemirror/view"
import { indentWithTab } from "@codemirror/commands"
If your language or extension isn't here, it's usually a one-line addition to
build/package.json followed by a rebuild — see below, or open an issue.
Working on the gem
git clone git@github.com:desheikh/codemirror.git
cd codemirror
npm --prefix build install
bundle install
The JavaScript is generated, not hand-written. build/build.mjs reads the
package list in build/package.json and writes both the modules and the pins:
npm --prefix build run build # -> app/assets/javascripts/codemirror/ + config/importmap.rb
MINIFY=0 npm --prefix build run build # unminified, for reading/debugging
There are two checks. check links the whole module graph the way a browser
would and builds a real editor state, which catches broken imports and the
duplicate-singleton problem. The Ruby tests make sure the pins and the shipped
files never drift apart.
npm --prefix build run check
bundle exec rake test
Bumping CodeMirror
npm --prefix build run update
This pulls the latest of every bundled package, rebuilds everything, and points
Codemirror::CODEMIRROR_VERSION at the new release (resetting
Codemirror::BUILD to 0 if the CodeMirror version actually changed). Look
over the diff, run the two checks, and you're good.
Versioning
The gem version follows the bundled CodeMirror version, with one extra number for our own builds on top of it:
6.0.2.0, 6.0.2.1, ... = <codemirror version>.<build>
CODEMIRROR_VERSION and BUILD live in lib/codemirror/version.rb. In
practice the build number does most of the moving, since upstream rarely bumps
the umbrella codemirror package even when the individual pieces change.
Releasing
Releases are automated. Push a commit that bumps the version to master and
the release workflow does the rest: it notices
Codemirror::VERSION isn't on RubyGems yet, then builds, pushes, tags
vX.Y.Z.B, and cuts a GitHub release. Merges that don't change the version are
ignored, so there's no harm in it running on every push.
So a normal release is just:
npm --prefix build run update(or editbuild/package.jsonand rebuild), and commit the regenerated assets.- Bump
Codemirror::BUILDif it's a new build for the same CodeMirror version. - Update
CHANGELOG.md. - Merge to
master.
License
MIT, same as CodeMirror itself. See LICENSE.