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 11 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
232 changes: 232 additions & 0 deletions app/assets/javascripts/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { closeBrackets, closeBracketsKeymap, autocompletion } 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,
indentUnit,
LanguageDescription,
syntaxHighlighting
} from "@codemirror/language";
import { languages } from "@codemirror/language-data";
import {

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

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L13-L14

Added lines #L13 - L14 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 24 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L24

Added line #L24 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.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());

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

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L150-L151

Added lines #L150 - L151 were not covered by tests
}
}),
LanguageDescription.of({
name: "Prolog",
alias: ["prolog"],
extensions: ["pl", "pro", "p"],
load() {
return import("codemirror-lang-prolog").then(m => m.prolog());

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

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L158-L159

Added lines #L158 - L159 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 167 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L166-L167

Added lines #L166 - L167 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 180 in app/assets/javascripts/editor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/editor.ts#L180

Added line #L180 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) {
console.log(languageSupport);
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