Summary: Remove repl.py, REPLTab.js, some wiring code, CSS, and a test in testscript.json. Test Plan: NA Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2923pull/9/head
parent
67aca9ccf6
commit
1f6e693b6e
@ -1,106 +0,0 @@
|
|||||||
.repl-container {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repl-text {
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre;
|
|
||||||
tab-size: 4;
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.repl-field {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.repl-error {
|
|
||||||
color: #D00;
|
|
||||||
}
|
|
||||||
.repl-text_line:hover {
|
|
||||||
background-color: #E5E5E5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.re-eval_line_button {
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
text-align: center;
|
|
||||||
width: 15px;
|
|
||||||
color: #808080;
|
|
||||||
}
|
|
||||||
.erase_line_button {
|
|
||||||
cursor: pointer;
|
|
||||||
float: right;
|
|
||||||
text-align: center;
|
|
||||||
width: 15px;
|
|
||||||
color: #808080;
|
|
||||||
}
|
|
||||||
.re-eval_line_button:hover {
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
.erase_line_button:hover {
|
|
||||||
color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unselectable {
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-khtml-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pointer_group {
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
width: 25px;
|
|
||||||
}
|
|
||||||
.pointer {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repl-newline {
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 2px 10px 15px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repl-cursor_editor {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.repl-text_editor {
|
|
||||||
position: absolute;
|
|
||||||
padding: 2px 0 0 0;
|
|
||||||
min-width: 10px;
|
|
||||||
min-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
resize: none;
|
|
||||||
box-shadow: none;
|
|
||||||
outline: none;
|
|
||||||
background: transparent;
|
|
||||||
z-index: 10;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repl-content_measure {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-right: 1em;
|
|
||||||
border: none;
|
|
||||||
visibility: hidden;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formula-text {
|
|
||||||
font-family: monospace;
|
|
||||||
tab-size: 4;
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
}
|
|
@ -1,318 +0,0 @@
|
|||||||
/**
|
|
||||||
* A Tab that contains a REPL.
|
|
||||||
* The REPL allows the user to write snippets of python code and see the results of evaluating
|
|
||||||
* them. In particular, the REPL has access to the usercode module, so they can see the results
|
|
||||||
* of quick operations on their data.
|
|
||||||
* The REPL supports evaluation of code, removal of lines from history, and re-computation
|
|
||||||
* and editing of older lines.
|
|
||||||
*/
|
|
||||||
var kd = require('../lib/koDom');
|
|
||||||
var ko = require('knockout');
|
|
||||||
var dom = require('../lib/dom');
|
|
||||||
var Base = require('./Base');
|
|
||||||
var commands = require('./commands');
|
|
||||||
|
|
||||||
var NEW_LINE = -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hard tab used instead of soft tabs, as soft-tabs would require a lot of additional
|
|
||||||
* editor logic (partial-width tabs, backspacing a tab, ...) for
|
|
||||||
* which we may want to eventually use a 3rd-party library for in addition to syntax highlighting, etc
|
|
||||||
*/
|
|
||||||
var INDENT_STR = "\t";
|
|
||||||
|
|
||||||
function REPLTab(gristDoc) {
|
|
||||||
Base.call(this, gristDoc);
|
|
||||||
this.replHist = gristDoc.docModel.replHist.createAllRowsModel("id");
|
|
||||||
this.docData = gristDoc.docData;
|
|
||||||
this.editingIndex = ko.observable(null);
|
|
||||||
this.histIndex = ko.observable(this.replHist.peekLength);
|
|
||||||
|
|
||||||
this.editorActive = ko.observable(false);
|
|
||||||
this.numLines = ko.observable(0);
|
|
||||||
this.row = null;
|
|
||||||
|
|
||||||
this._contentSizer = ko.observable('');
|
|
||||||
this._originalValue = '';
|
|
||||||
this._textInput = null;
|
|
||||||
|
|
||||||
this.commandGroup = this.autoDispose(commands.createGroup(
|
|
||||||
REPLTab.replCommands, this, this.editorActive));
|
|
||||||
}
|
|
||||||
|
|
||||||
Base.setBaseFor(REPLTab);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Editor commands for the cellEditor in the REPL Tab
|
|
||||||
* TODO: Using the command group, distinguish between "on enter" saves and "on blur" saves
|
|
||||||
* So that we can give up focus on blur
|
|
||||||
*/
|
|
||||||
REPLTab.replCommands = {
|
|
||||||
// TODO: GridView commands are activated more recently after startup.
|
|
||||||
fieldEditSave: function() {
|
|
||||||
if (!this._textInput || !this.editorActive() ||
|
|
||||||
!this._textInput.value.trim() && this.editingIndex() === NEW_LINE) { return; }
|
|
||||||
// TODO: Scroll pane does not automatically scroll down on save.
|
|
||||||
var self = this;
|
|
||||||
this.save()
|
|
||||||
.then(function(success) {
|
|
||||||
if (success) {
|
|
||||||
self.editingIndex(NEW_LINE);
|
|
||||||
self.clear();
|
|
||||||
// Refresh the history index.
|
|
||||||
self.histIndex(self.replHist.peekLength);
|
|
||||||
} else {
|
|
||||||
self.write("\n");
|
|
||||||
// Since focus is staying in the current input, increment lines.
|
|
||||||
self.numLines(self.numLines.peek()+1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fieldEditCancel: function() {
|
|
||||||
this.clear();
|
|
||||||
this.editingIndex(NEW_LINE);
|
|
||||||
},
|
|
||||||
nextField: function() {
|
|
||||||
// In this case, 'nextField' (Tab) inserts a tab.
|
|
||||||
this.write(INDENT_STR);
|
|
||||||
},
|
|
||||||
historyPrevious: function() {
|
|
||||||
// Fills the editor with the code previously entered.
|
|
||||||
if (this.editingIndex() === NEW_LINE) { this.writePrev(); }
|
|
||||||
},
|
|
||||||
historyNext: function() {
|
|
||||||
// Fills the editor with the code entered after the current code.
|
|
||||||
if (this.editingIndex() === NEW_LINE) { this.writeNext(); }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the entered code as an EvalCode Useraction.
|
|
||||||
* @param {Function} callback - Is called with a single argument 'success' indicating
|
|
||||||
* whether the save was successful.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.save = function(callback) {
|
|
||||||
if (!this._textInput.value.trim()) {
|
|
||||||
// If its text is cleared, remove history item.
|
|
||||||
var currentEditIndex = this.editingIndex();
|
|
||||||
this.histIndex(this.replHist.peekLength - 1);
|
|
||||||
this.editorActive(false);
|
|
||||||
return this.docData.sendAction(["RemoveRecord", "_grist_REPL_Hist", currentEditIndex]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// If something is entered, save value.
|
|
||||||
var rowId = this.row ? this.row.id() : null;
|
|
||||||
return this.docData.sendAction(["EvalCode", this._textInput.value, rowId]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Builds object with REPLTab dom builder and settings for the sidepane.
|
|
||||||
REPLTab.prototype.buildConfigDomObj = function() {
|
|
||||||
return [{
|
|
||||||
'buildDom': this.buildDom.bind(this),
|
|
||||||
'keywords': ['repl', 'console', 'python', 'code', 'terminal']
|
|
||||||
}];
|
|
||||||
};
|
|
||||||
|
|
||||||
REPLTab.prototype.buildDom = function() {
|
|
||||||
var self = this;
|
|
||||||
return dom('div',
|
|
||||||
kd.foreach(this.replHist, function(replLine) {
|
|
||||||
return dom('div.repl-container',
|
|
||||||
dom('div.repl-text_line',
|
|
||||||
kd.scope(function() { return self.editingIndex() === replLine.id(); },
|
|
||||||
function(isEditing) {
|
|
||||||
if (isEditing) {
|
|
||||||
return dom('div.field.repl-field',
|
|
||||||
kd.scope(self.numLines, function(numLines) {
|
|
||||||
return self.buildPointerGroup(numLines);
|
|
||||||
}),
|
|
||||||
self.attachEditorDom(replLine));
|
|
||||||
} else {
|
|
||||||
var numLines = replLine.code().trim().split('\n').length;
|
|
||||||
return dom('div.repl-field',
|
|
||||||
dom.on('click', function() {
|
|
||||||
// TODO: Flickering occurs on click for multiline code segments.
|
|
||||||
self.editingIndex(replLine.id());
|
|
||||||
self.focus();
|
|
||||||
}),
|
|
||||||
self.buildPointerGroup(numLines),
|
|
||||||
dom('div.repl-text',
|
|
||||||
kd.text(replLine.code)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
dom('div.erase_line_button.unselectable', dom.on('click', function() {
|
|
||||||
self.histIndex(self.replHist.peekLength - 1);
|
|
||||||
return self.docData.sendAction(
|
|
||||||
["RemoveRecord", "_grist_REPL_Hist", replLine.id()]
|
|
||||||
);
|
|
||||||
}), '\u2A09'),
|
|
||||||
dom('div.re-eval_line_button.unselectable', dom.on('click', function() {
|
|
||||||
return self.docData.sendAction(
|
|
||||||
["EvalCode", replLine.code(), replLine.id()]
|
|
||||||
);
|
|
||||||
}), '\u27f3') // 'refresh' symbol
|
|
||||||
),
|
|
||||||
kd.maybe(replLine.outputText, function() {
|
|
||||||
return dom('div.repl-text.repl-output', kd.text(replLine.outputText));
|
|
||||||
}),
|
|
||||||
kd.maybe(replLine.errorText, function() {
|
|
||||||
return dom('div.repl-text.repl-error', kd.text(replLine.errorText));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
// Special bottom editor which sends actions to add new records to the REPL hist.
|
|
||||||
dom('div.repl-newline',
|
|
||||||
dom.on('click', function() {
|
|
||||||
self.editingIndex(NEW_LINE);
|
|
||||||
self.focus();
|
|
||||||
}),
|
|
||||||
dom('div.field.repl-field',
|
|
||||||
kd.scope(self.numLines, function(numLines) {
|
|
||||||
return self.buildPointerGroup(self.editingIndex() === NEW_LINE ? numLines : 1);
|
|
||||||
}),
|
|
||||||
kd.maybe(ko.pureComputed(function() { return self.editingIndex() === NEW_LINE; }),
|
|
||||||
function() { return self.attachEditorDom(null); }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the set of pointers to the left of the code
|
|
||||||
* @param {String} code - The code for which the pointer group is to be built.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.buildPointerGroup = function(numLines) {
|
|
||||||
var pointers = [];
|
|
||||||
for (var i = 0; i < numLines; i++) {
|
|
||||||
pointers.push(dom('div.pointer', i ? '...' : '>>>'));
|
|
||||||
}
|
|
||||||
return dom('div.pointer_group.unselectable', pointers);
|
|
||||||
};
|
|
||||||
|
|
||||||
REPLTab.prototype.buildEditorDom = function() {
|
|
||||||
var self = this;
|
|
||||||
return dom('div.repl-cursor_editor',
|
|
||||||
dom('div.repl-content_measure.formula-text', kd.text(this._contentSizer)),
|
|
||||||
function() {
|
|
||||||
self._textInput = dom('textarea.repl-text_editor.formula-text',
|
|
||||||
kd.value(self.row ? self.row.code() : ""),
|
|
||||||
dom.on('focus', function() {
|
|
||||||
self.numLines(this.value.split('\n').length);
|
|
||||||
}),
|
|
||||||
dom.on('blur', function() {
|
|
||||||
if (!this._textInput || !this.editorActive()) { return; }
|
|
||||||
self.save()
|
|
||||||
.then(function(success) {
|
|
||||||
if (success) {
|
|
||||||
// If editing a new line, clear it to start fresh.
|
|
||||||
if (self.editingIndex() === NEW_LINE) { self.clear(); }
|
|
||||||
// Refresh the history index.
|
|
||||||
self.histIndex(self.replHist.peekLength);
|
|
||||||
} else {
|
|
||||||
self.write("\n");
|
|
||||||
}
|
|
||||||
self.editorActive(false);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
//Resizes the textbox whenever user writes in it.
|
|
||||||
dom.on('input', function() {
|
|
||||||
self.numLines(this.value.split('\n').length);
|
|
||||||
self.resizeElem();
|
|
||||||
}),
|
|
||||||
dom.defer(function(elem) {
|
|
||||||
self.resizeElem();
|
|
||||||
elem.focus();
|
|
||||||
// Set the cursor at the end.
|
|
||||||
var elemLen = elem.value.length;
|
|
||||||
elem.selectionStart = elemLen;
|
|
||||||
elem.selectionEnd = elemLen;
|
|
||||||
}),
|
|
||||||
dom.on('mouseup mousedown click', function(event) { event.stopPropagation(); }),
|
|
||||||
self.commandGroup.attach()
|
|
||||||
);
|
|
||||||
return self._textInput;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function measures a hidden div with the same value as the textarea being edited and then resizes the textarea to match.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.resizeElem = function() {
|
|
||||||
// \u200B is a zero-width space; it is used so the textbox will expand vertically
|
|
||||||
// on newlines, but it does not add any width the string
|
|
||||||
this._contentSizer(this._textInput.value + '\u200B');
|
|
||||||
var rect = this._textInput.parentNode.childNodes[0].getBoundingClientRect();
|
|
||||||
//Allows form to expand passed its container div.
|
|
||||||
this._textInput.style.width = Math.ceil(rect.width) + 'px';
|
|
||||||
this._textInput.style.height = Math.ceil(rect.height) + 'px';
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends text to the contents being edited
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.write = function(text) {
|
|
||||||
this._textInput.value += text;
|
|
||||||
this.resizeElem();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears both the current text and any memory of text in the currently edited cell.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.clear = function() {
|
|
||||||
this._textInput.value = "";
|
|
||||||
this._orignalValue = "";
|
|
||||||
this.numLines(1);
|
|
||||||
this.resizeElem();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores focus to the most recent input.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.focus = function() {
|
|
||||||
if (this._textInput) {
|
|
||||||
this._textInput.focus();
|
|
||||||
this.editorActive(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the code entered before the current code to the input.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.writePrev = function() {
|
|
||||||
this.histIndex(Math.max(this.histIndex.peek() - 1, 0));
|
|
||||||
this.clear();
|
|
||||||
if (this.replHist.at(this.histIndex.peek())) {
|
|
||||||
this.write(this.replHist.at(this.histIndex.peek()).code());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the code entered after the current code to the input.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.writeNext = function() {
|
|
||||||
this.histIndex(Math.min(this.histIndex() + 1, this.replHist.peekLength));
|
|
||||||
this.clear();
|
|
||||||
if (this.histIndex.peek() < this.replHist.peekLength) {
|
|
||||||
this.write(this.replHist.at(this.histIndex.peek()).code());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called in the DOM element where an editor is desired.
|
|
||||||
* It attaches to as a child of that element with that elements value as default or whatever is set as an override value.
|
|
||||||
*/
|
|
||||||
REPLTab.prototype.attachEditorDom = function(row) {
|
|
||||||
var self = this;
|
|
||||||
self.row = row;
|
|
||||||
self._originalValue = self.row ? self.row.code() : "";
|
|
||||||
return self.buildEditorDom();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = REPLTab;
|
|
@ -1,8 +0,0 @@
|
|||||||
import {DocModel, IRowModel} from 'app/client/models/DocModel';
|
|
||||||
|
|
||||||
// Record of input code and output text and error info for REPL.
|
|
||||||
export type REPLRec = IRowModel<"_grist_REPL_Hist">
|
|
||||||
|
|
||||||
export function createREPLRec(this: REPLRec, docModel: DocModel): void {
|
|
||||||
// no extra fields
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
"""
|
|
||||||
This module implements an interpreter for a REPL. It subclasses Python's
|
|
||||||
code.InteractiveInterpreter class, implementing most of its methods, but with
|
|
||||||
slight changes in order to be convenient for Grist's purposes
|
|
||||||
"""
|
|
||||||
|
|
||||||
import code
|
|
||||||
import sys
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
SUCCESS = 0
|
|
||||||
INCOMPLETE = 1
|
|
||||||
ERROR = 2
|
|
||||||
|
|
||||||
EvalTuple = namedtuple("EvalTuple", ("output", "error", "status"))
|
|
||||||
|
|
||||||
#pylint: disable=exec-used, bare-except
|
|
||||||
class REPLInterpreter(code.InteractiveInterpreter):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
code.InteractiveInterpreter.__init__(self)
|
|
||||||
self.error_text = ""
|
|
||||||
|
|
||||||
def runsource(self, source, filename="<input>", symbol="single"):
|
|
||||||
"""
|
|
||||||
Compiles and executes source. Returns an EvalTuple with a status
|
|
||||||
INCOMPLETE if the code is incomplete,
|
|
||||||
ERROR if it encountered a compilation or run-time error,
|
|
||||||
SUCCESS otherwise.
|
|
||||||
|
|
||||||
an output, which gives all of the output of the user's program
|
|
||||||
(with stderr piped to stdout, essentially, though mock-file objects are used)
|
|
||||||
|
|
||||||
an error, which reports a syntax error at compilation or a runtime error with a
|
|
||||||
Traceback.
|
|
||||||
"""
|
|
||||||
old_stdout = sys.stdout
|
|
||||||
old_stderr = sys.stderr
|
|
||||||
|
|
||||||
user_output = six.StringIO()
|
|
||||||
|
|
||||||
self.error_text = ""
|
|
||||||
try:
|
|
||||||
code = self.compile(source, filename, symbol)
|
|
||||||
except (OverflowError, SyntaxError, ValueError):
|
|
||||||
self.showsyntaxerror(filename)
|
|
||||||
status = ERROR
|
|
||||||
else:
|
|
||||||
status = INCOMPLETE if code is None else SUCCESS
|
|
||||||
|
|
||||||
if status == SUCCESS:
|
|
||||||
|
|
||||||
try:
|
|
||||||
# We use temproray variables to access stdio/stdout
|
|
||||||
# to make sure the client can't do funky things
|
|
||||||
# like get/set attr and have that hurt us
|
|
||||||
sys.stdout = user_output
|
|
||||||
sys.stderr = user_output
|
|
||||||
exec(code, self.locals)
|
|
||||||
except:
|
|
||||||
# bare except to catch absolutely all things the user can throw
|
|
||||||
self.showtraceback()
|
|
||||||
status = ERROR
|
|
||||||
finally:
|
|
||||||
sys.stdout = old_stdout
|
|
||||||
sys.stderr = old_stderr
|
|
||||||
|
|
||||||
program_output = user_output.getvalue()
|
|
||||||
try:
|
|
||||||
user_output.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return EvalTuple(program_output, self.error_text, status)
|
|
||||||
|
|
||||||
def write(self, txt):
|
|
||||||
"""
|
|
||||||
Used by showsyntaxerror and showtraceback
|
|
||||||
"""
|
|
||||||
self.error_text += txt
|
|
||||||
|
|
||||||
def runcode(self, code):
|
|
||||||
"""
|
|
||||||
This would normally do the part of runsource after compiling the code, but doesn't quite
|
|
||||||
make sense as its own function for our purposes because it couldn't support an INCOMPLETE
|
|
||||||
return value, etc. We explicitly hide it here to make sure the base class's version isn't
|
|
||||||
called by accident.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("REPLInterpreter.runcode not implemented, use runsource instead")
|
|
Loading…
Reference in new issue