-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use CodeMirror 6 instead of relying on <textarea>. This provides support for syntax highlighting, better auto-indentation, line numbers, and the option to introduce more features (code folding, vi keybindings, etc) in the future if needed. I picked CodeMirror 6 mainly because it supports mobile devices. The two others that I tried (Monaco, Ace) don't work well on mobile. Ace kinda works but has limitations, Monaco doesn't work at all. In addition, CM6 is relatively small while the others tend to require a lot of JavaScript. For more background, see: https://blog.replit.com/code-editors
- Loading branch information
Showing
11 changed files
with
807 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
release.tar.gz | ||
worker/webworker.bundle.js | ||
/node_modules | ||
/resources/editor.bundle.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { EditorState, Compartment } from '@codemirror/state'; | ||
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'; | ||
import { indentUnit, bracketMatching, syntaxHighlighting, defaultHighlightStyle, HighlightStyle } from '@codemirror/language'; | ||
import { closeBrackets } from '@codemirror/autocomplete'; | ||
import { lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, highlightActiveLine, keymap, EditorView } from '@codemirror/view'; | ||
|
||
import { oneDark } from "@codemirror/theme-one-dark"; | ||
import { tango } from "./tango.js"; | ||
|
||
import { go } from "@codemirror/lang-go"; | ||
|
||
// This is a CodeMirror 6 editor. | ||
// The editor features are wrapped to provide a simpler interface for the | ||
// simulator. | ||
export class Editor { | ||
// Create a new editor, which will be added to the given parent. | ||
constructor(parent) { | ||
this.parent = parent; | ||
this.view = null; | ||
this.modifyCallback = () => {}; | ||
this.parentStyles = getComputedStyle(parent); | ||
|
||
// Detect dark mode from theme changes. | ||
matchMedia('(prefers-color-scheme: dark)').onchange = () => { | ||
this.#setDarkMode(this.#getDarkMode()); | ||
}; | ||
|
||
// Detect dark mode from changes in the <html> attributes (e.g. | ||
// data-bs-theme="..."). | ||
new MutationObserver(() => { | ||
this.#setDarkMode(this.#getDarkMode()); | ||
}).observe(document.documentElement, {attributes: true}); | ||
} | ||
|
||
// Set (or replace) the callback to call when the text in the editor changed. | ||
// The changed text can be obtained using the text() method. | ||
setModifyCallback(callback) { | ||
this.modifyCallback = callback; | ||
} | ||
|
||
// Return the current text in the editor. | ||
text() { | ||
if (!this.view) { | ||
throw 'editor was not set up yet (need to call setText() first?)'; | ||
} | ||
return this.view.state.doc.toString(); | ||
} | ||
|
||
// Replace the text in the editor. This resets the editor state entirely, | ||
// including the undo history. | ||
setText(text) { | ||
const editorState = this.#createEditorState(text, this.modifyCallback); | ||
|
||
// Create a new view, or if it already exists, replace the state in the view. | ||
if (!this.view) { | ||
this.view = new EditorView({ | ||
state: editorState, | ||
parent: this.parent, | ||
}); | ||
} else { | ||
this.view.setState(editorState); | ||
} | ||
} | ||
|
||
#createEditorState(initialContents) { | ||
this.darkMode = this.#getDarkMode(); | ||
|
||
this.themeConfig = new Compartment(); | ||
let extensions = [ | ||
EditorView.updateListener.of(update => { | ||
if (update.changedRanges.length) { | ||
this.modifyCallback(); | ||
} | ||
}), | ||
lineNumbers(), | ||
highlightActiveLineGutter(), | ||
highlightSpecialChars(), | ||
history(), | ||
drawSelection(), | ||
EditorView.lineWrapping, | ||
indentUnit.of("\t"), | ||
bracketMatching(), | ||
closeBrackets(), | ||
highlightActiveLine(), | ||
keymap.of([ | ||
...defaultKeymap, | ||
...historyKeymap, | ||
]), | ||
go(), | ||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }), | ||
this.themeConfig.of(this.#getDarkStyle(this.darkMode)), | ||
]; | ||
|
||
return EditorState.create({ | ||
doc: initialContents, | ||
extensions | ||
}); | ||
} | ||
|
||
// Get the array of extensions (with the theme) depending on whether we're | ||
// currently in dark mode or not. | ||
#getDarkStyle(dark) { | ||
return dark ? [oneDark] : [tango]; | ||
} | ||
|
||
// Return whether the editor parent node is currently in a dark mode or not. | ||
#getDarkMode() { | ||
// Extract the 3 RGB numbers from styles.color. | ||
let parts = this.parentStyles.color.match(RegExp('\\d+', 'g')); | ||
// The following is a simplified version of the math found in here to | ||
// calculate whether styles.color is light or dark. | ||
// https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color/56678483#56678483 | ||
// Approximate linear sRGB. | ||
let r = Math.pow(parseInt(parts[0]) / 255, 2.2); | ||
let g = Math.pow(parseInt(parts[1]) / 255, 2.2); | ||
let b = Math.pow(parseInt(parts[2]) / 255, 2.2); | ||
// Calculate luminance (in linear sRGB space). | ||
let luminance = (0.2126*r + 0.7152*g + 0.0722*b); | ||
// Check whether text luminance is above the "middle grey" threshold of | ||
// 18.4% (which probably means there's light text on a dark background, aka | ||
// dark mode). | ||
let isDark = luminance > 0.184; | ||
return isDark; | ||
} | ||
|
||
// Update the editor with the given dark mode. | ||
#setDarkMode(dark) { | ||
if (dark !== this.darkMode) { | ||
this.darkMode = dark; | ||
this.view.dispatch({ | ||
effects: this.themeConfig.reconfigure(this.#getDarkStyle(dark)), | ||
}) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'; | ||
import { EditorView } from '@codemirror/view'; | ||
import {tags as t} from "@lezer/highlight" | ||
|
||
// The Tango theme, based on: | ||
// https://github.com/alecthomas/chroma/blob/master/styles/tango.xml | ||
// But this theme in turn came from Pygments: | ||
// https://github.com/pygments/pygments/blob/master/pygments/styles/tango.py | ||
// | ||
// There are a number of differences between the Chroma (and presumably | ||
// Pygments) theme, and the CodeMirror theme here: | ||
// | ||
// - Lezer (the CodeMirror parser) doesn't expose built-in functions (like | ||
// println) and types (like int), so they can't be themed. This is an | ||
// intentional design choice: | ||
// https://github.com/lezer-parser/go/issues/1 | ||
// - Lezer doesn't distinguish between '=' and ':='. It also doesn't | ||
// distinguish between the address-taking operand '&' and the field operand | ||
// '.'. | ||
// | ||
// Overall I've tried to keep the semantic meaning of the highlighter the same | ||
// as the original in Chroma (and Pygments): | ||
// | ||
// - I use the bold orange color for operands that feel important: assignments | ||
// (both ':=' and '='), increment/decrement operators ('++', '--'), and | ||
// logic operators ('||', '&&'). | ||
// - For all other operands and punctuation, I've picked a black bold style. | ||
|
||
export const tangoHighlightStyle = HighlightStyle.define([ | ||
{tag: t.keyword, | ||
color: "#204a87", // dark blue (bold) | ||
fontWeight: "bold"}, | ||
{tag: t.comment, | ||
color: "#8f5902", // brown-ish orange | ||
fontStyle: "italic"}, | ||
{tag: t.string, | ||
color: "#4e9a06"}, // light green | ||
{tag: t.number, | ||
color: "#0000cf", // bright blue | ||
fontWeight: "bold"}, | ||
{tag: [t.paren, t.squareBracket, t.brace, t.punctuation, t.operator], | ||
fontWeight: "bold"}, // black | ||
{tag: [t.modifier, t.definitionOperator, t.updateOperator, t.logicOperator], | ||
color: "#ce5c00", // orange | ||
fontWeight: "bold"}, | ||
]); | ||
|
||
export const tangoTheme = EditorView.theme({ | ||
"&": { | ||
backgroundColor: "#f8f8f8", | ||
}, | ||
}) | ||
|
||
export const tango = [tangoTheme, syntaxHighlighting(tangoHighlightStyle)]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.