stem-core
Version:
Frontend and core-library framework
493 lines (407 loc) • 13.4 kB
JSX
// Wrapper over the Ace code editor, needs ace to be loaded
// TODO: should be renamed to AceCodeEditor?
import {UI} from "./UIBase";
import {StyleSheet, styleRule} from "./Style";
import {registerStyle} from "./style/Theme";
import {EnqueueableMethodMixin, enqueueIfNotLoaded} from "../base/EnqueueableMethodMixin";
class CodeEditor extends EnqueueableMethodMixin(UI.Element) {
static requireAce(callback) {
throw Error("You need to implement requireAce");
}
isLoaded() {
return !!this.getAce();
}
setOptions(options) {
let defaultOptions = {
aceMode: "text",
readOnly: false,
aceTheme: "dawn",
aceKeyboardHandler: "ace",
fontSize: 14,
tabSize: 4,
showLineNumber: true,
showPrintMargin: false,
printMarginSize: 80,
};
options = Object.assign(defaultOptions, options);
super.setOptions(options);
if (this.options.aceMode) {
this.options.aceMode = this.options.aceMode.toLowerCase();
}
if (this.options.aceMode === "cpp" || this.options.aceMode === "c") {
this.options.aceMode = "c_cpp";
}
if (this.getAce()) {
this.applyAceOptions();
}
}
redraw() {
if (this.getAce()) {
this.aceResize();
this.applyRef();
return;
}
super.redraw();
}
whenLoaded(callback) {
if (this.isLoaded()) {
callback();
} else {
this.addListenerOnce("aceReady", callback);
}
}
onMount() {
// Sometimes when the parent div resizes the ace editor doesn't fully update.
this.addListener("resize", () => {
this.aceResize();
});
this.addListener("change", () => {
this.aceResize();
});
if (!window.ace) {
this.constructor.requireAce(() => {
this.onDelayedMount();
});
return;
}
this.onDelayedMount();
}
onDelayedMount() {
this.ace = window.ace.edit(this.node);
// Removes some warnings
this.getAce().$blockScrolling = Infinity;
this.resolveQueuedMethods();
this.applyAceOptions();
//#voodoo was here to automatically redraw when unhiding
//This Ace event listener might be useful in the future
this.getAce().renderer.$textLayer.addEventListener("changeCharacterSize", (event) => {
this.aceResize();
});
this.dispatch("aceReady");
}
getAce() {
return this.ace;
}
getValue() {
return this.getAce().getValue();
}
applyAceOptions() {
//set the language mode
this.setAceMode(this.options.aceMode);
this.setAceKeyboardHandler(this.options.aceKeyboardHandler);
this.setAceTheme(this.options.aceTheme);
this.setAceFontSize(this.options.fontSize);
this.setAceTabSize(this.options.tabSize);
this.setAceLineNumberVisible(this.options.showLineNumber);
this.setAcePrintMarginVisible(this.options.showPrintMargin);
this.setAcePrintMarginSize(this.options.printMarginSize);
this.setReadOnly(this.options.readOnly);
this.setUseWrapMode(this.options.lineWrapping || false);
if (this.options.numLines) {
this.options.maxLines = this.options.minLines = this.options.numLines;
}
if (this.options.maxLines) {
this.setAceOptions({
maxLines: this.options.maxLines
});
}
if (this.options.minLines) {
this.setAceOptions({
minLines: this.options.minLines
});
}
if (this.options.value) {
this.setValue(this.options.value, -1);
}
if (this.options.hasOwnProperty("enableBasicAutocompletion") ||
this.options.hasOwnProperty("enableLiveAutocompletion")) {
let langTools ="/static/js/ext/ace/ext-language_tools.js";
require([langTools], () => {
this.setBasicAutocompletion(this.options.enableBasicAutocompletion);
this.setLiveAutocompletion(this.options.enableLiveAutocompletion);
this.setSnippets(this.options.enableSnippets);
});
}
}
aceResize() {
this.getAce().resize();
}
setValue(sourceCode, fakeUserChange) {
// We need to wrap the ace call in these flags so any event listeners can know if this change
// was done by us or by the user
this.apiChange = !fakeUserChange;
this.getAce().setValue(sourceCode, -1);
this.apiChange = false;
}
setAceOptions(options) {
this.getAce().setOptions(options);
}
// TODO: should this be setEditable?
setReadOnly(value) {
this.getAce().setReadOnly(value);
};
setAceMode(aceMode) {
if (aceMode.hasOwnProperty("aceMode")) {
aceMode = aceMode.aceMode;
}
this.getAce().getSession().setMode("ace/mode/" + aceMode);
}
getAceKeyboardHandler() {
return this.getAce().$keybindingId;
}
setAceKeyboardHandler(keyboardHandler) {
if (keyboardHandler.hasOwnProperty("aceName")) {
keyboardHandler = keyboardHandler.aceName;
}
this.getAce().setKeyboardHandler("ace/keyboard/" + keyboardHandler);
}
getAceMode() {
return this.getAce().getSession().getMode();
}
setAceTheme(theme) {
if (theme.hasOwnProperty("aceName")) {
theme = theme.aceName;
}
this.getAce().setTheme("ace/theme/" + theme);
}
getAceTheme() {
return this.getAce().getTheme();
}
setAceFontSize(fontSize) {
this.getAce().setOptions({
fontSize: fontSize + "px"
});
}
getAceFontSize() {
return this.getAce().getFontSize();
}
setAceTabSize(tabSize) {
this.getAce().setOptions({
tabSize: tabSize
});
}
getAceTabSize() {
return this.getAce().getOption("tabSize");
}
setAceLineNumberVisible(value) {
this.getAce().renderer.setShowGutter(value);
}
getAceLineNumberVisible() {
return this.getAce().renderer.getShowGutter();
}
setAcePrintMarginVisible(value) {
this.getAce().setShowPrintMargin(value);
}
getAcePrintMarginVisible() {
return this.getAce().getShowPrintMargin();
}
setAcePrintMarginSize(printMarginSize) {
this.getAce().setPrintMarginColumn(printMarginSize);
}
getAcePrintMarginSize() {
return this.getAce().getPrintMarginColumn();
}
setBasicAutocompletion(value) {
this.getAce().setOptions({
enableBasicAutocompletion: value
});
}
setLiveAutocompletion(value) {
this.getAce().setOptions({
enableLiveAutocompletion: value
});
}
setSnippets(value) {
this.getAce().setOptions({
enableSnippets: value
});
}
setAnnotations(annotations) {
this.getAce().getSession().setAnnotations(annotations);
}
setUseWrapMode(value) {
this.getAce().getSession().setUseWrapMode(value);
}
setIndentedSoftWrap(value) {
this.getAce().setOption("indentedSoftWrap", value);
}
blockScroll() {
this.getAce().$blockScrolling = Infinity;
}
setFoldStyle(foldStyle) {
this.getAce().getSession().setFoldStyle(foldStyle);
}
setHighlightActiveLine(value) {
this.getAce().setHighlightActiveLine(value);
}
setHighlightGutterLine(value) {
this.getAce().setHighlightGutterLine(value);
}
setShowGutter(value) {
this.getAce().renderer.setShowGutter(value);
}
getScrollTop() {
return this.getAce().getSession().getScrollTop();
}
setScrollTop(value) {
this.getAce().getSession().setScrollTop(value);
}
addMarker(startLine, startCol, endLine, endCol, ...args) {
const Range = this.constructor.AceRange;
return this.getAce().getSession().addMarker(new Range(startLine, startCol, endLine, endCol), ...args);
}
removeMarker(marker) {
this.getAce().getSession().removeMarker(marker);
}
getRendererLineHeight() {
return this.getAce().renderer.lineHeight;
}
getTextRange(startLine, startCol, endLine, endCol) {
const Range = this.constructor.AceRange;
return this.getAce().getSession().doc.getTextRange(new Range(startLine, startCol, endLine, endCol));
}
setTextRange(startLine, startCol, endLine, endCol, text) {
const Range = this.constructor.AceRange;
this.getAce().getSession().replace(new Range(startLine, startCol, endLine, endCol), text);
}
removeLine(line) {
const Range = this.constructor.AceRange;
this.getAce().getSession().getDocument().remove(new Range(line, 0, line + 1, 0));
}
insertAtLine(line, str) {
let column = this.getAce().session.getLine(line - 1).length;
this.getAce().gotoLine(line, column);
this.insert(str);
}
replaceLine(line, str) {
const Range = this.constructor.AceRange;
this.getAce().getSession().getDocument().replace(new Range(line, 0, line + 1, 0), str);
}
addAceSessionEventListener(event, callback) {
this.getAce().getSession().addEventListener(event, callback);
}
addAceSessionChangeListener(callback) {
this.addAceSessionEventListener("change", callback);
}
addAceChangeListener(callback) {
this.getAce().on("change", callback);
}
addAceEventListener() {
this.getAce().addEventListener(...arguments);
}
focus() {
this.getAce().focus();
}
gotoEnd() {
let editor = this.getAce();
let editorRow = editor.session.getLength() - 1;
let editorColumn = editor.session.getLine(editorRow).length;
editor.gotoLine(editorRow + 1, editorColumn);
}
setUndoManager(undoManager) {
this.getAce().getSession().setUndoManager(undoManager);
}
setAceRendererOption(key, value) {
this.getAce().renderer.setOption(key, value);
}
// Inserts the text at the current cursor position
insert(text) {
this.getAce().insert(text);
}
// Appends the text at the end of the document
append(text) {
var lastRow = this.getAce().getSession().getLength() - 1;
if (lastRow < 0) {
lastRow = 0;
}
var lastRowLength = this.getAce().getSession().getLine(lastRow).length;
var scrolledToBottom = this.getAce().isRowFullyVisible(lastRow);
// console.log("Scroll to bottom ", scrolledToBottom);
this.getAce().getSession().insert({
row: lastRow,
column: lastRowLength
}, text);
this.aceResize();
if (scrolledToBottom) {
// TODO: Include scroll lock option!
// TODO: See if scrolling to bottom can be done better
// TODO: for some reason the scroll bar height is not being updated, this needs to be fixed
this.getAce().scrollToLine(this.getAce().getSession().getLength() - 1, true, true, function () {});
}
}
copyTextToClipboard() {
this.getAce().selectAll();
this.getAce().focus();
document.execCommand('copy');
}
}
class StaticCodeHighlighterStyle extends StyleSheet {
hideActive = {
" .ace_gutter-active-line": {
display: "none",
},
" .ace_active-line": {
display: "none",
},
" .ace_cursor": {
display: "none",
}
}
}
class StaticCodeHighlighter extends CodeEditor {
setOptions(options) {
options = Object.assign({
fontSize: 13,
readOnly: true,
lineWrapping: true,
}, options);
super.setOptions(options);
}
extraNodeAttributes(attr) {
attr.addClass(this.styleSheet.hideActive)
}
}
export {CodeEditor, StaticCodeHighlighter};