Skip to content

Commit

Permalink
Merge pull request #4970 from dodona-edu/enhance/codemirror
Browse files Browse the repository at this point in the history
Replace Ace Editor with CodeMirror 6
  • Loading branch information
jorg-vr authored Oct 30, 2023
2 parents f65dceb + e1b00b0 commit 1853849
Show file tree
Hide file tree
Showing 23 changed files with 1,458 additions and 1,162 deletions.
1 change: 0 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
app/assets/config/manifest.js
app/assets/javascripts/ace_editor.js
app/assets/javascripts/i18n/translations.js
app/assets/javascripts/types/index.d.ts
app/assets/javascripts/inputServiceWorker.js
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ updates:
rails:
patterns:
- "@rails*"
codemirror:
patterns:
- "*codemirror*"
- "@lezer*"
testing-library:
patterns:
- "@testing-library*"
Expand Down
3 changes: 0 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ gem 'builder', '~>3.2.4'
# generate diffs
gem 'diff-lcs', '~>1.5'

# code editor
gem 'ace-rails-ap', '~>4.5'

# auto css prefixer
gem 'autoprefixer-rails', '~>10.4.15'

Expand Down
2 changes: 0 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
GEM
remote: https://rubygems.org/
specs:
ace-rails-ap (4.5)
actioncable (7.1.1)
actionpack (= 7.1.1)
activesupport (= 7.1.1)
Expand Down Expand Up @@ -558,7 +557,6 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
ace-rails-ap (~> 4.5)
after_commit_everywhere (~> 1.3.1)
annotate (~> 3.2.0)
autoprefixer-rails (~> 10.4.15)
Expand Down
1 change: 0 additions & 1 deletion app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
//= link_tree ../images
//= link ace_editor.js
//= link_tree ../builds
11 changes: 0 additions & 11 deletions app/assets/javascripts/ace_editor.js

This file was deleted.

18 changes: 6 additions & 12 deletions app/assets/javascripts/coding_scratchpad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@ import { Papyros } from "@dodona/papyros";
import { InputMode } from "@dodona/papyros";
import { ProgrammingLanguage } from "@dodona/papyros";
import { themeState } from "state/Theme";

/**
* Custom interface to not have to add the ace package as dependency
*/
interface Editor {
setValue(v: string): void;
getValue(): string;
}
import { EditorView } from "@codemirror/view";
import { setCode } from "editor";

/** Identifiers used in HTML for relevant elements */
const CODE_EDITOR_PARENT_ID = "scratchpad-editor-wrapper";
Expand All @@ -25,7 +19,7 @@ const SUBMIT_TAB_ID = "activity-handin-link";
function initCodingScratchpad(programmingLanguage: ProgrammingLanguage): void {
if (Papyros.supportsProgrammingLanguage(programmingLanguage)) {
let papyros: Papyros | undefined = undefined;
let editor: Editor | undefined = undefined;
let editor: EditorView | undefined = undefined;
const closeButton = document.getElementById(CLOSE_BUTTON_ID);
// To prevent horizontal scrollbar issues, we delay rendering the button
// until after the page is loaded
Expand All @@ -46,14 +40,14 @@ function initCodingScratchpad(programmingLanguage: ProgrammingLanguage): void {
});
editor ||= window.dodona.editor;
if (editor) {
// Shortcut to copy code to ACE editor
// Shortcut to copy code to editor
papyros.addButton(
{
id: CODE_COPY_BUTTON_ID,
buttonText: I18n.t("js.coding_scratchpad.copy_code")
},
() => {
editor.setValue(papyros.getCode());
setCode(editor, papyros.getCode());
closeButton.click();
// Open submit panel if possible
document.getElementById(SUBMIT_TAB_ID)?.click();
Expand Down Expand Up @@ -88,7 +82,7 @@ function initCodingScratchpad(programmingLanguage: ProgrammingLanguage): void {
document.getElementById(OFFCANVAS_ID).addEventListener("shown.bs.offcanvas", () => {
editor ||= window.dodona.editor;
if (editor) { // Start with code from the editor, if there is any
const editorCode = editor.getValue();
const editorCode = editor.state.doc.toString();
const currentCode = papyros.getCode();
if (!currentCode || // Papyros empty
// Neither code areas are empty, but they differ
Expand Down
231 changes: 231 additions & 0 deletions app/assets/javascripts/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { closeBrackets, closeBracketsKeymap, autocompletion } from "@codemirror/autocomplete";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import {
bracketMatching,
foldGutter,
foldKeymap,
HighlightStyle,
indentOnInput,
indentUnit,
LanguageDescription,
syntaxHighlighting
} from "@codemirror/language";
import { languages } from "@codemirror/language-data";
import {
drawSelection,
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers
} from "@codemirror/view";
import { tags } from "@lezer/highlight";
import { Extension } from "@codemirror/state";

declare type EditorEventHandler = (event: FocusEvent, view: EditorView) => boolean | void;


// A custom theme for CodeMirror that applies the same CSS as Rouge does,
// meaning we can use our existing themes.
const rougeStyle = HighlightStyle.define([
{ tag: tags.comment, class: "c" },
{ tag: tags.lineComment, class: "c" },
{ tag: tags.blockComment, class: "cm" },
{ tag: tags.docComment, class: "cs" },
{ tag: tags.name, class: "n" },
{ tag: tags.standard(tags.variableName), class: "nb" },
{ tag: tags.standard(tags.propertyName), class: "nb" },
{ tag: tags.special(tags.variableName), class: "nb" },
{ tag: tags.special(tags.propertyName), class: "nb" },
{ tag: tags.function(tags.propertyName), class: "nf" },
{ tag: tags.function(tags.variableName), class: "nf" },
{ tag: tags.variableName, class: "nv" },
{ tag: tags.typeName, class: "kt" },
{ tag: tags.tagName, class: "nt" },
{ tag: tags.propertyName, class: "py" },
{ tag: tags.attributeName, class: "na" },
{ tag: tags.className, class: "nc" },
{ tag: tags.labelName, class: "nl" },
{ tag: tags.namespace, class: "nn" },
{ tag: tags.macroName, class: "n" },
{ tag: tags.literal, class: "l" },
{ tag: tags.string, class: "s" },
{ tag: tags.docString, class: "sd" },
{ tag: tags.character, class: "sc" },
{ tag: tags.attributeValue, class: "g" },
{ tag: tags.number, class: "m" },
{ tag: tags.integer, class: "mi" },
{ tag: tags.float, class: "mf" },
{ tag: tags.bool, class: "l" },
{ tag: tags.regexp, class: "sr" },
{ tag: tags.escape, class: "se" },
{ tag: tags.color, class: "l" },
{ tag: tags.url, class: "l" },
{ tag: tags.keyword, class: "k" },
{ tag: tags.self, class: "k" },
{ tag: tags.null, class: "l" },
{ tag: tags.atom, class: "l" },
{ tag: tags.unit, class: "l" },
{ tag: tags.modifier, class: "g" },
{ tag: tags.operatorKeyword, class: "ow" },
{ tag: tags.controlKeyword, class: "k" },
{ tag: tags.definitionKeyword, class: "kd" },
{ tag: tags.moduleKeyword, class: "kn" },
{ tag: tags.operator, class: "o" },
{ tag: tags.derefOperator, class: "o" },
{ tag: tags.arithmeticOperator, class: "o" },
{ tag: tags.logicOperator, class: "o" },
{ tag: tags.bitwiseOperator, class: "o" },
{ tag: tags.compareOperator, class: "o" },
{ tag: tags.updateOperator, class: "o" },
{ tag: tags.definitionOperator, class: "o" },
{ tag: tags.typeOperator, class: "o" },
{ tag: tags.controlOperator, class: "o" },
{ tag: tags.punctuation, class: "p" },
{ tag: tags.separator, class: "dl" },
{ tag: tags.bracket, class: "p" },
{ tag: tags.angleBracket, class: "p" },
{ tag: tags.squareBracket, class: "p" },
{ tag: tags.paren, class: "p" },
{ tag: tags.brace, class: "p" },
{ tag: tags.content, class: "g" },
{ tag: tags.heading, class: "gh" },
{ tag: tags.heading1, class: "gu" },
{ tag: tags.heading2, class: "gu" },
{ tag: tags.heading3, class: "gu" },
{ tag: tags.heading4, class: "gu" },
{ tag: tags.heading5, class: "gu" },
{ tag: tags.heading6, class: "gu" },
{ tag: tags.contentSeparator, class: "dl" },
{ tag: tags.list, class: "p" },
{ tag: tags.quote, class: "p" },
{ tag: tags.emphasis, class: "ge" },
{ tag: tags.strong, class: "gs" },
{ tag: tags.link, class: "g" },
{ tag: tags.monospace, class: "go" },
{ tag: tags.strikethrough, class: "gst" },
{ tag: tags.inserted, class: "gi" },
{ tag: tags.deleted, class: "gd" },
{ tag: tags.changed, class: "g" },
{ tag: tags.invalid, class: "err" },
{ tag: tags.meta, class: "c" }
]);

// Basic, built-in extensions.
const editorSetup = (() => [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
indentUnit.of(" "),
indentOnInput(),
bracketMatching(),
closeBrackets(),
highlightActiveLine(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...foldKeymap,
]),
syntaxHighlighting(rougeStyle, {
fallback: true
}),
autocompletion()
])();


// The "@codemirror/language-data" does not support community languages,
// so we add support for those ourselves.
const additionalLanguages = [
LanguageDescription.of({
name: "R",
alias: ["rlang"],
extensions: ["r"],
load() {
return import("codemirror-lang-r").then(m => m.r());
}
}),
LanguageDescription.of({
name: "Prolog",
alias: ["prolog"],
extensions: ["pl", "pro", "p"],
load() {
return import("codemirror-lang-prolog").then(m => m.prolog());
}
}),
LanguageDescription.of({
name: "C#",
alias: ["csharp", "cs"],
extensions: ["cs"],
load() {
return import("@replit/codemirror-lang-csharp").then(m => m.csharp());
}
}),
];


async function loadProgrammingLanguage(language: string): Promise<Extension | undefined> {
const potentialLanguages = additionalLanguages.concat(languages);
const description = LanguageDescription.matchLanguageName(potentialLanguages, language);
if (description) {
await description.load();
return description.support;
}
console.warn(`${language} is not supported by our editor, falling back to nothing.`);
}

/**
* Set up the code editor.
*
* @param parent The element to insert the editor into. Existing content will be inserted into the editor.
* @param programmingLanguage The programming language of the editor. Will attempt to load language support.
* @param focusHandler A callback that will be called when the editor receives focus.
*/
export async function configureEditor(parent: Element, programmingLanguage: string, focusHandler: EditorEventHandler): Promise<EditorView> {
const existingCode = parent.textContent;
// Clear the existing code, as we will put it in CodeMirror.
parent.textContent = "";
const eventHandlers = EditorView.domEventHandlers({
"focus": focusHandler
});
const languageSupport = await loadProgrammingLanguage(programmingLanguage);
const languageExtensions = [];
if (languageSupport !== undefined) {
languageExtensions.push(languageSupport);
}
return new EditorView({
doc: existingCode,
extensions: [
// Basic editor functionality
editorSetup,
// Listen for focus
eventHandlers,
// Language support
...languageExtensions
],
parent: parent
});
}


/**
* Set the content of a code editor.
*
* @param editorView The code editor to set the content in.
* @param code The code to insert.
*/
export function setCode(editorView: EditorView, code: string): void {
editorView.dispatch(editorView.state.update({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: code,
}
}));
}
Loading

0 comments on commit 1853849

Please sign in to comment.