Wednesday, July 20, 2022

Line highlighting extension for Code Mirror 6

A little background: Dis This is my online tool for viewing the disassembled bytecode of Python snippets. I started it off with a simple text editor, but I wanted to upgrade it to a nice code editor with line numbers and syntax highlighting.

The most commonly used online code editor libraries are Monaco, CodeMirror, and ACE. I believe Monaco is the most full featured and accessible, but I opted to try CodeMirror for this project as I don’t need as many features. (I avoided ACE since its what we used for the Khan Academy coding environment, and we found it fairly buggy).

CodeMirror recently released a new version, v6, and its quite different architecturally from previous versions.

One of those differences is that the library can only be loaded as a module and cannot be loaded via CDN, so my first task was adding module bundling via rollup.

Once I got rollup running, it was fairly straightforward to get a basic editor working:

import {basicSetup} from 'codemirror';
import {EditorState} from '@codemirror/state';
import {python} from '@codemirror/lang-python';
import {EditorView} from '@codemirror/view';

const editorView = new EditorView({
    state: EditorState.create({
        doc: code,
        extensions: [basicSetup, python()],
    }),
    parent: document.getElementById(“editor”),
});

But now I wanted a new feature: bi-directional line highlighting. Whenever a user highlighted a line in the editor, it should highlight relevant rows in the bytecode table, and vice versa. The end goal:

To try to understand CodeMirror's new approach to extensibility, I did a lot of reading in the docs: Migration Guide, System Guide, Decorations, Zebra Stripes, etc. Here's the code I came up with.

First I make a Decoration of the line variety:

const lineHighlightMark = Decoration.line({
  attributes: {style: 'background-color: yellow'}
});

Then I define a StateEffect:

const addLineHighlight = StateEffect.define();

Tying those together, I define a StateField. When the field receives an addLineHighlight effect, it clears existing decorations and adds the line decoration to the desired line:

const lineHighlightField = StateField.define({
  create() {
    return Decoration.none;
  },
  update(lines, tr) {
    lines = lines.map(tr.changes);
    for (let e of tr.effects) {
      if (e.is(addLineHighlight)) {
        lines = Decoration.none;
        lines = lines.update({add: [lineHighlightMark.range(e.value)]});
      }
    }
    return lines;
  },
  provide: (f) => EditorView.decorations.from(f),
});

To be able to use that effect, I add it to the list of extensions in the original editor constructor:

extensions: [basicSetup, python(), lineHighlightField],

Now I need to setup each direction of line highlighting. To enable highlighting when a user moves their mouse over the code editor, I add an event listener which converts the mouse position to a line number, converts the line number to a “document position”, then dispatches the addLineHighlight effect:

editorView.dom.addEventListener('mousemove', (event) => {
    const lastMove = {
        x: event.clientX,
        y: event.clientY,
        target: event.target,
        time: Date.now(),
    };
    const pos = this.editorView.posAtCoords(lastMove);
    let lineNo = this.editorView.state.doc.lineAt(pos).number;
    const docPosition = this.editorView.state.doc.line(lineNo).from;
    this.editorView.dispatch({effects: addLineHighlight.of(docPosition)});
});

To enable highlighting when the user mouses over rows in the corresponding HTML table, I call a function that converts the line number to a document position and dispatches the effect (same as the last two lines of the previous code).

function highlightLine(lineNo) {
    const docPosition = this.editorView.state.doc.line(lineNo).from;
    this.editorView.dispatch({effects: addLineHighlight.of(docPosition)});
}

For ease of use, I wrap all that code into a HighlightableEditor class:

editor = new HighlightableEditor(codeDiv, code});

Check out the full highlightable-editor.js code on Github.

No comments: