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:
Post a Comment