Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Ace Editor with CodeMirror 6 #4970

Merged
merged 13 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { 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";

Check warning on line 6 in app/assets/javascripts/coding_scratchpad.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/coding_scratchpad.ts#L6

Added line #L6 was not covered by tests

/** Identifiers used in HTML for relevant elements */
const CODE_EDITOR_PARENT_ID = "scratchpad-editor-wrapper";
Expand All @@ -25,7 +19,7 @@
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 @@
});
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());

Check warning on line 50 in app/assets/javascripts/coding_scratchpad.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/coding_scratchpad.ts#L50

Added line #L50 was not covered by tests
closeButton.click();
// Open submit panel if possible
document.getElementById(SUBMIT_TAB_ID)?.click();
Expand Down Expand Up @@ -88,7 +82,7 @@
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
221 changes: 221 additions & 0 deletions app/assets/javascripts/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import {

Check warning on line 3 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L1-L3

Added lines #L1 - L3 were not covered by tests
bracketMatching,
foldGutter,
foldKeymap,
HighlightStyle,
indentOnInput, LanguageDescription,
syntaxHighlighting
} from "@codemirror/language";
import { languages } from "@codemirror/language-data";
import {

Check warning on line 12 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L11-L12

Added lines #L11 - L12 were not covered by tests
drawSelection,
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers
} from "@codemirror/view";
import { tags } from "@lezer/highlight";

Check warning on line 22 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L22

Added line #L22 was not covered by tests
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.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(),
indentOnInput(),
bracketMatching(),
closeBrackets(),
highlightActiveLine(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
...foldKeymap,
]),
syntaxHighlighting(rougeStyle, {
fallback: true
})
])();


// 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());

Check warning on line 141 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L140-L141

Added lines #L140 - L141 were not covered by tests
}
}),
LanguageDescription.of({
name: "Prolog",
alias: ["rlang"],
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved
extensions: ["pl", "pro", "p"],
load() {
return import("codemirror-lang-prolog").then(m => m.prolog());

Check warning on line 149 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L148-L149

Added lines #L148 - L149 were not covered by tests
}
}),
LanguageDescription.of({
name: "C#",
alias: ["csharp", "cs"],
extensions: ["cs"],
load() {
return import("@replit/codemirror-lang-csharp").then(m => m.csharp());

Check warning on line 157 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L156-L157

Added lines #L156 - L157 were not covered by tests
}
}),
];


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.`);

Check warning on line 170 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L170

Added line #L170 was not covered by tests
}

/**
* 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
Loading