summaryrefslogtreecommitdiff
path: root/js/codeq
diff options
context:
space:
mode:
Diffstat (limited to 'js/codeq')
-rw-r--r--js/codeq/login.js112
-rw-r--r--js/codeq/mainScreen.js126
-rw-r--r--js/codeq/prolog.js448
-rw-r--r--js/codeq/prologPythonLib.js341
-rw-r--r--js/codeq/python.js339
-rw-r--r--js/codeq/startup.js6
-rw-r--r--js/codeq/stateMachine.js22
7 files changed, 1392 insertions, 2 deletions
diff --git a/js/codeq/login.js b/js/codeq/login.js
new file mode 100644
index 0000000..5c93469
--- /dev/null
+++ b/js/codeq/login.js
@@ -0,0 +1,112 @@
+/**
+ * Created by robert on 9/17/15.
+ */
+
+(function(){
+ var loginFun = function(){
+ var identifier = $('#problem_group').val().split('/'),
+ problem = $('#problems').val();
+ if (identifier.length < 2) alert('Choose a problem group');
+ else if (!problem) alert('Choose a problem');
+ else {
+ $('#disabled').css('display', '');
+ codeq.comms.login($('#username').val(), $('#password').val())
+ .then(function (data) {
+ $('#disabled').css('display', 'none');
+ if (data.code !== 0) throw new Error('Login failed, code: ' + data.code + ', message: ' + data.message);
+ })
+ .then(function () {
+ return codeq.comms.getProblem(identifier[0], identifier[1], problem);
+ })
+ .then(function (data) {
+ if (data.code !== 0) throw new Error('Failed to obtain problem data, code: ' + data.code + ', message: ' + data.message);
+ $('#disabled').css('display', 'none');
+ switch (identifier[0]) {
+ case 'prolog':
+ // TODO: assignment to window for debug only
+ //$('#screen_login').css('display', 'none');
+ //$('#screen_prolog').css('display', '');
+ codeq.globalStateMachine.transition('prolog', data);
+ //window.phandler = codeq.createProgrammingLanguageHandler('prolog',data.data,codeq.makePrologTerminalHandler);//codeq.createPrologHandler(data.data);
+ break;
+ case 'python':
+ // TODO: assignment to window for debug only
+ //$('#screen_login').css('display', 'none');
+ //$('#screen_prolog').css('display', '');
+ codeq.globalStateMachine.transition('python', data);
+ //window.phandler = codeq.createProgrammingLanguageHandler('python',data.data,codeq.makePythonTerminalHandler);//codeq.createPythonHandler(data.data);
+ break;
+ default:
+ alert('Unknown language: ' + identifier[0]);
+ break;
+ }
+ })
+ .fail(function (reason) {
+ $('#disabled').css('display', 'none');
+ alert('Login request failed: ' + reason);
+ })
+ .done();
+ }
+ };
+ codeq.globalStateMachine.register('login',{
+ 'enter': function(){
+ codeq.comms.connect().then(function () {
+ return codeq.comms.send({'action': 'list_problems'});//currently problem list and the actual login are still in the same state
+ }).then(
+ function success(data) {
+ var i, groups, group, problems, problem, first_group,
+ jqGroup = $('#problem_group'),
+ jqProblems = $('#problems'),
+ html = [],
+ mapping = {},
+ onGroupChange = function () {
+ var problems = mapping[jqGroup.val()],
+ html = [],
+ i, p;
+ if (problems) {
+ for (i = 0; i < problems.length; i++) {
+ p = problems[i];
+ html.push('<option value="', p.identifier, '">', p.name, '</option>\n')
+ }
+ }
+ jqProblems.html(html.join(''));
+ };
+
+ if (data && (data.code === 0)) {
+ $('#disabled').css('display', 'none');
+ groups = data.problems;
+ for (i = 0; i < groups.length; i++) {
+ group = groups[i];
+ var identifier = group.identifier.language + '/' + group.identifier.group;
+ var name = group.name.language + ': ' + group.name.group;
+ html.push('<option value="', identifier, '">', name, '</option>\n');
+ mapping[identifier] = group.problems;
+ }
+ jqGroup.html(html.join(''));
+ first_group = html[1];
+ html = null;
+
+ jqGroup.on('click', onGroupChange);
+ jqGroup.val(first_group);
+ onGroupChange();
+
+ $("#submit").on('click', loginFun);
+ }
+ else {
+ $('#disabled').css('cursor', '');
+ alert('Obtaining list of problems failed: code=' + data.code + ', reason=' + data.message);
+ }
+ },
+
+ function failure(reason) {
+ $('#disabled').css('cursor', '');
+ alert('Request to obtain list of problems failed: ' + reason);
+ }
+ ).done();
+ },
+ 'exit' : function(){
+ $("#submit").off('click', loginFun);
+ $("#screen_login").css('display', 'none');
+ }
+ });
+})(); \ No newline at end of file
diff --git a/js/codeq/mainScreen.js b/js/codeq/mainScreen.js
new file mode 100644
index 0000000..e842ee3
--- /dev/null
+++ b/js/codeq/mainScreen.js
@@ -0,0 +1,126 @@
+/**
+ * Created by robert on 9/17/15.
+ */
+
+(function() {
+ var problems,//this will the actual (sub)state machine
+ stateNameTag = 'stateName';
+
+ var divs = {};
+ divs['description'] = $('div.col-lg-3.col-md-6.col-sm-12:has(#description)');//lets actually find the needed divs for later use
+ divs['code'] = $('div.col-lg-3.col-md-6.col-sm-12:has(#code_editor)');
+ divs['console'] = $('div.col-lg-3.col-md-6.col-sm-12:has(#console)');//named 'consoleDiv', because 'console' is already in use
+ divs['info'] = $('div.col-lg-3.col-md-6.col-sm-12:has(#info)');
+
+ divs['description'].data(stateNameTag, 'description');
+ divs['code'].data(stateNameTag, 'code');
+ divs['console'].data(stateNameTag, 'console');
+ divs['info'].data(stateNameTag, 'info');
+
+ var eventName = 'mousedown',//event name of the event which will trigger the transition between these substates
+ mouseDownEventFunction = function () {
+ problems.transition($(this).data(stateNameTag));
+ },
+ removeListenersFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.off(eventName, mouseDownEventFunction);
+ });
+ },
+ removeBlockClassesFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.removeClass('block');
+ });
+ },
+ removeChangedClassesFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.removeClass('block-focus block-less-width block-less-height block-less-everything').addClass('block');//these class names were chosen for the view where the screen is partitioned in 4 quarters (2 by 2)
+ });
+ },
+ setTransitionsBetweenDivs = function (current) {
+ $.each(divs, function (key, value) {
+ if (current !== key) value.on(eventName, mouseDownEventFunction);
+ });
+ },
+ addClassesToDivs = function (divsWithClasses) {
+ $.each(divsWithClasses, function (divName, className) {
+ divs[divName].addClass(className);
+ });
+ },
+ substates = {
+ 'description': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-focus',
+ 'code': 'block-less-width',
+ 'console': 'block-less-height',
+ 'info': 'block-less-everything'
+ });
+ setTransitionsBetweenDivs('description');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ },
+ 'code': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-width',
+ 'code': 'block-focus',
+ 'console': 'block-less-everything',
+ 'info': 'block-less-height'
+ });
+ setTransitionsBetweenDivs('code');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ },
+ 'info': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-everything',
+ 'code': 'block-less-height',
+ 'console': 'block-less-width',
+ 'info': 'block-focus'
+ });
+ setTransitionsBetweenDivs('info');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ },
+ 'console': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-height',
+ 'code': 'block-less-everything',
+ 'console': 'block-focus',
+ 'info': 'block-less-width'
+ });
+ setTransitionsBetweenDivs('console');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ }
+ };
+ codeq.globalStateMachine.register('main', {
+ 'enter': function () {
+ $('#screen_prolog').css('display', '');
+ problems = codeq.makeStateMachine(substates);
+ problems.transition(divs['description'].data(stateNameTag));
+ },
+ 'exit': function () {
+ $('#screen_prolog').css('display', 'none');
+ problems.destroy();
+ }
+ });
+})(); \ No newline at end of file
diff --git a/js/codeq/prolog.js b/js/codeq/prolog.js
new file mode 100644
index 0000000..6c53777
--- /dev/null
+++ b/js/codeq/prolog.js
@@ -0,0 +1,448 @@
+/**
+ * Created by robert on 9/17/15.
+ */
+
+(function() {
+ var problems,//this will the actual (sub)state machine
+ stateNameTag = 'stateName';
+
+ //get the divs which are the main elements being change on transitions between states in the sub-state machine
+ var divs = {};
+ divs['description'] = $('#description_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#description)');
+ divs['code'] = $('#code_editor_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#code_editor)');
+ divs['console'] = $('#console_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#console)');
+ divs['info'] = $('#info_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#info)');
+
+ //these tags connect the div to their respective state in the sub-state machine
+ divs['description'].data(stateNameTag, 'description');
+ divs['code'].data(stateNameTag, 'code');
+ divs['console'].data(stateNameTag, 'console');
+ divs['info'].data(stateNameTag, 'info');
+
+ var transitionEventName = 'mousedown',//event name of the event which will trigger the transition between these substates - the most common transition at least (there are some corner cases on the hint and test buttons -> see the code below)
+ /**
+ * the general function which should be called on the mousedown event to trigger the transition between states
+ */
+ mouseDownEventFunction = function () {
+ problems.transition($(this).data(stateNameTag));
+ },
+ /**
+ * removes the above function from the divs
+ */
+ removeListenersFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.off(transitionEventName, mouseDownEventFunction);
+ });
+ },
+ /**
+ * removes the 'block' class from all divs we need to change when we change the current state
+ */
+ removeBlockClassesFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.removeClass('block');
+ });
+ },
+ /**
+ * removes all those special classes from the divs, which are set when a new state is entered and it also adds the 'block' class back
+ */
+ removeChangedClassesFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.removeClass('block-focus block-less-width block-less-height block-less-everything').addClass('block');//these class names were chosen for the view where the screen is partitioned in 4 quarters (2 by 2)
+ });
+ },
+ /**
+ *
+ * @param current the key of the state ('description','code','hint','console'), which is currently active, because it must not have a transition set
+ */
+ setTransitionsBetweenDivs = function (current) {
+ $.each(divs, function (key, value) {
+ if (current !== key) value.on(transitionEventName, mouseDownEventFunction);
+ });
+ },
+ /**
+ *
+ * @param divsWithClasses a object where we have the keys of the divs object paired with the class name we want to assign to them e.g. :
+ * {
+ 'description': 'block-focus',
+ 'code': 'block-less-width',
+ 'console': 'block-less-height',
+ 'info': 'block-less-everything'
+ }
+ */
+ addClassesToDivs = function (divsWithClasses) {
+ $.each(divsWithClasses, function (divName, className) {
+ divs[divName].addClass(className);
+ });
+ },
+ /**
+ * the buttons (hint and test) will have to 'eat' the mousedown event to prevent some weird transitions.
+ * Because a click on them won't trigger the transition to the code part, but will transition to the hints part.
+ *
+ * @param event
+ */
+ mouseDownEventIgnoreFun = function(event){
+ event.stopPropagation();
+ }
+ /**
+ * The transition from the buttons to the hints screen will be triggered with this function
+ */
+ clickListenerTransitionFun = function(){
+ problems.transition(divs['info'].data(stateNameTag));
+ },
+ /**
+ * add the above function to the buttons
+ */
+ addClickListenerTranstions = function(){
+ $('#btn_code_hint').on('click',clickListenerTransitionFun);
+ $('#btn_code_test').on('click',clickListenerTransitionFun);
+
+ $('#btn_code_hint').on(transitionEventName,mouseDownEventIgnoreFun);
+ $('#btn_code_test').on(transitionEventName,mouseDownEventIgnoreFun);
+ },
+ /**
+ * and a function to remove it
+ */
+ removeClickListenerTransition = function(){
+ $('#btn_code_hint').off('click',clickListenerTransitionFun);
+ $('#btn_code_test').off('click',clickListenerTransitionFun);
+
+ $('#btn_code_hint').off(transitionEventName,mouseDownEventIgnoreFun);
+ $('#btn_code_test').off(transitionEventName,mouseDownEventIgnoreFun);
+ },
+ substates = {
+ 'description': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-focus',
+ 'code': 'block-less-width',
+ 'console': 'block-less-height',
+ 'info': 'block-less-everything'
+ });
+ setTransitionsBetweenDivs('description');
+
+ //and set up the buttons
+ addClickListenerTranstions();
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ removeClickListenerTransition();
+ }
+ },
+ 'code': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-width',
+ 'code': 'block-focus',
+ 'console': 'block-less-everything',
+ 'info': 'block-less-height'
+ });
+ setTransitionsBetweenDivs('code');
+
+ //and set up the buttons
+ addClickListenerTranstions();
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ removeClickListenerTransition();
+ }
+ },
+ 'info': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-everything',
+ 'code': 'block-less-height',
+ 'console': 'block-less-width',
+ 'info': 'block-focus'
+ });
+ setTransitionsBetweenDivs('info');
+
+ //and set up the buttons
+ addClickListenerTranstions();
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ removeClickListenerTransition();
+ }
+ },
+ 'console': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-height',
+ 'code': 'block-less-everything',
+ 'console': 'block-focus',
+ 'info': 'block-less-width'
+ });
+ setTransitionsBetweenDivs('console');
+
+ //and set up the buttons
+ addClickListenerTranstions();
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ removeClickListenerTransition();
+ }
+ }
+ };
+ var prologHandler;//created when we enter the prolog state and destroyed once we leave it
+ codeq.globalStateMachine.register('prolog', {
+ 'enter': function (data) {
+ $('#disabled').css('display', '');
+ $('#screen_prolog').css('display', '');//we have to show the screen now so the code editor shopws its initial values correctly
+ prologHandler = createPrologHandler(data.data)//codeq.createProgrammingLanguageHandler('prolog',data.data,codeq.makePrologTerminalHandler);
+ problems = codeq.makeStateMachine(substates);
+ problems.transition(divs['description'].data(stateNameTag));
+ Q.delay(100).then(function(){
+ $('div.col-lg-3.col-md-6.col-sm-12').addClass('transition');//for smooth animations - need to be delayed, because otherwise we get some weird "animations" while the page is loading
+ }).done();
+
+ $('#disabled').css('display', 'none');
+ },
+ 'exit': function () {
+ $('#screen_prolog').css('display', 'none');
+ $('div.col-lg-3.col-md-6.col-sm-12').removeClass('transition');
+ prologHandler.destroy();
+ problems.destroy();
+ }
+ });
+
+ // a constant
+ var firstCharacterPos = {'line': 0, 'ch': 0};
+
+ var makePrologTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) {
+ var promptMode = true, // default: query composition; alternative: query result browsing
+ terminal = codeq.makeConsole(jqConsole, {
+ 'greeting': 'CodeQ Prolog terminal proxy'
+ }),
+ tcs = function terminalCommandSuccess (data) {
+ var t, lines, i;
+ if (data.code === 0) {
+ t = data.terminal;
+ terminal.append(t.messages.join('\n'), 'output');
+ promptMode = !t.have_more;
+ }
+ else {
+ terminal.append(data.message, 'error');
+ promptMode = true;
+ }
+ if (promptMode) {
+ terminal.setLineBuffered();
+ terminal.append('.\n?- ', 'output');
+ }
+ },
+ tcf = function terminalCommandFailed (error) {
+ promptMode = true;
+ terminal.setLineBuffered();
+ terminal.append(error + '\n', 'error');
+ terminal.append('?- ', 'output');
+ };
+
+ terminal.onKeypress = function (c) {
+ if (promptMode) return c; // query composition: return the character unchanged
+ switch (c) {
+ case ' ':
+ case ';':
+ case '\n':
+ return ';'; // show next answer on space, semicolon or enter
+ default:
+ return '.'; // everything else: stop searching for answers
+ }
+ };
+
+ terminal.onInput = function (command) {
+ if (promptMode) {
+ promptMode = false;
+ terminal.setNotBuffered();
+ return codeq.comms.sendQuery({
+ 'problem_id': problem_id,
+ 'step': 'run',
+ 'program': editor.getDoc().getValue(),
+ 'query': command,
+ 'trace': activityHandler.addAndPurge({'typ': 'slv', 'qry': command})
+ }, problem_id).then(tcs, tcf);
+ }
+ else {
+ terminal.append('\n', 'input');
+ if (command == ';') {
+ // show next answer
+ return codeq.comms.sendQuery({
+ 'problem_id': problem_id,
+ 'step': 'next',
+ 'trace': activityHandler.addAndPurge({'typ': 'nxt'})
+ }, problem_id).then(tcs, tcf);
+ }
+ else {
+ // stop searching for answers
+ return codeq.comms.sendQuery({
+ 'problem_id': problem_id,
+ 'step': 'end',
+ 'trace': activityHandler.addAndPurge({'typ': 'stp'})
+ }, problem_id).then(tcs, tcf);
+ }
+
+ }
+ };
+
+ terminal.leftmostCol = 3;
+ terminal.append('?- ', 'output');
+
+ return terminal;
+ };
+
+ var makeActivityHandler = function (editor, problem_id) {
+ var lastActivityMillis = Date.now(),
+ deltaActivityMillis = function deltaActivityMillisFunc () {
+ var now = Date.now(),
+ dt = now - lastActivityMillis;
+ lastActivityMillis = now;
+ return dt;
+ },
+ queue = [],
+ ts = null,
+ timer = function () {
+ var promise;
+ ts = null;
+ if (queue.length === 0) return Q(true);
+ promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id);
+ queue.length = 0;
+ return promise;
+ },
+ flush = function () {
+ clearTimeout(ts);
+ return timer();
+ };
+
+ return {
+ 'queueTrace': function (trace) {
+ trace['dt'] = deltaActivityMillis();
+ queue.push(trace);
+ if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds
+ return this;
+ },
+ 'flush': flush,
+ 'addAndPurge': function (trace) {
+ var accumulatedTrace = queue;
+ queue = [];
+ trace['dt'] = deltaActivityMillis();
+ accumulatedTrace.push(trace);
+ if (ts !== null) {
+ clearTimeout(ts);
+ ts = null;
+ }
+ return accumulatedTrace;
+ }
+ };
+ };
+
+ /**
+ * Creates a new handler for the given Prolog assignment definition.
+ *
+ * @param {PrologTaskDef} info
+ * @returns {{destroy: Function, processServerHints: Function}}
+ */
+ createPrologHandler = function (info) {
+ var problem = info.problem,
+ jqDescription = $('#description'),
+ jqEditor = $('#code_editor'),
+ jqConsole = $('#console'),
+ jqHints = $('#info'),
+ editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }),
+ activityHandler = makeActivityHandler(editor, problem.id),
+ terminal = makePrologTerminalHandler(jqConsole, editor, problem.id, activityHandler),
+ hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint);
+
+ editor.setValue(info.solution);
+ $('#title').text(problem.slug);
+ jqDescription.html(problem.description);
+
+ editor.on('change', function (instance, changeObj) {
+ var doc = editor.getDoc(),
+ pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from));
+
+ if (changeObj.removed) {
+ activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'len': codeq.codePointCount(changeObj.removed)});
+ }
+
+ if (changeObj.text) {
+ activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text});
+ }
+ });
+
+ $('#btn_code_hint').on('click', function () {
+ terminal.append('hint.\n', 'input');
+ terminal.inputDisable();
+ var doc = editor.getDoc();
+ codeq.comms.sendHint({
+ 'language': 'prolog',
+ 'program': editor.getDoc().getValue(),
+ 'problem_id': problem.id
+ })
+ .then(
+ function hintSuccess(data) {
+ if (data.code === 0)
+ hinter.handle(data.hints);
+ else
+ terminal.append(data.message + '\n', 'error');
+ },
+ function hintFailed (error) {
+ terminal.append(error + '\n', 'error');
+ }
+ )
+ .fin(function () {
+ terminal.inputEnable();
+ terminal.append('?- ', 'output');
+ })
+ .done();
+ });
+ $('#btn_code_test').on('click', function () {
+ terminal.append('test.\n', 'input');
+ terminal.inputDisable();
+ var doc = editor.getDoc();
+ codeq.comms.sendTest({
+ 'language': 'prolog',
+ 'program': editor.getDoc().getValue(),
+ 'problem_id': problem.id
+ })
+ .then(
+ function testSuccess(data) {
+ if (data.code === 0)
+ hinter.handle(data.hints);
+ else
+ terminal.append(data.message + '\n', 'error');
+ },
+ function testFailed (error) {
+ terminal.append(error + '\n', 'error');
+ }
+ )
+ .fin(function () {
+ terminal.inputEnable();
+ terminal.append('?- ', 'output');
+ })
+ .done();
+ });
+
+ return {
+ destroy: function () {
+ $('#btn_code_hint').off('click');
+ $('#btn_code_test').off('click');
+ editor.off('change');
+ hinter.destroy();
+ terminal.destroy();
+ jqDescription.empty();
+ jqEditor.empty(); // TODO: perhaps you do not want to "free" the editor, just empty it
+ jqConsole.empty(); // TODO: the same with the console
+ jqDescription = null;
+ jqEditor = null;
+ jqConsole = null;
+ jqHints = null;
+ }
+ };
+ };
+})(); \ No newline at end of file
diff --git a/js/codeq/prologPythonLib.js b/js/codeq/prologPythonLib.js
new file mode 100644
index 0000000..400a56e
--- /dev/null
+++ b/js/codeq/prologPythonLib.js
@@ -0,0 +1,341 @@
+/**
+ * Created by robert on 9/15/15.
+ */
+
+/**
+ * this is currently not used - I made this common python/prolog library a bit to early, but I don't want to delete everything, so I'll leave it for now in case it can be used later without to many changes
+ *
+ *
+ *
+ * @param editor
+ * @param problem_id
+ * @returns {{queueTrace: Function, flush: Function, addAndPurge: Function}}
+ */
+
+codeq.makeActivityHandler = function (editor, problem_id) {
+ var lastActivityMillis = Date.now(),
+ deltaActivityMillis = function deltaActivityMillisFunc () {
+ var now = Date.now(),
+ dt = now - lastActivityMillis;
+ lastActivityMillis = now;
+ return dt;
+ },
+ queue = [],
+ ts = null,
+ timer = function () {
+ var promise;
+ ts = null;
+ if (queue.length === 0) return Q(true);
+ promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id);
+ queue.length = 0;
+ return promise;
+ },
+ flush = function () {
+ clearTimeout(ts);
+ return timer();
+ };
+
+ return {
+ 'queueTrace': function (trace) {
+ trace['dt'] = deltaActivityMillis();
+ queue.push(trace);
+ if (ts === null) setTimeout(timer, 10000); // flush every 10 seconds
+ },
+ 'flush': flush,
+ 'addAndPurge': function (trace) {
+ var accumulatedTrace = queue;
+ queue = [];
+ trace['dt'] = deltaActivityMillis();
+ accumulatedTrace.push(trace);
+ if (ts !== null) {
+ clearTimeout(ts);
+ ts = null;
+ }
+ return accumulatedTrace;
+ }
+ };
+};
+
+/**
+ * Creates a new handler for the given Programming language assignment definition.
+ *
+ * @param {string} language - either 'prolog' or 'python'
+ * @param {PrologTaskDef} info
+ * @param {function} makeTerminalHandlerFun - either the makePythonTerminalHandler or makePrologTerminalHandler function
+ * @returns {{destroy: Function, processServerHints: Function}}
+ */
+codeq.createProgrammingLanguageHandler = function (language,info,makeTerminalHandlerFun) {
+ var firstCharacterPos = {'line': 0, 'ch': 0};
+
+ var problem = info.problem,
+ jqDescription = $('#description'),
+ jqEditor = $('#code_editor'),
+ jqConsole = $('#console'),
+ jqHints = $('#info'),
+ editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true }),
+ activityHandler = codeq.makeActivityHandler(editor, problem.id),
+ terminal = makeTerminalHandlerFun(jqConsole, editor, problem.id, activityHandler),
+ hintDefs = problem.hint,
+ hintCounter = 0, // for generating unique class-names
+ hintCleaners = [],
+ clearHints = function () {
+ var i;
+ for (i = hintCleaners.length - 1; i >= 0; i--) {
+ hintCleaners[i]();
+ }
+ hintCleaners.length = 0;
+ hintCounter = 0;
+ },
+ addMark = function (start, end) {
+ var posStart = editor.posFromIndex(start),
+ posEnd = editor.posFromIndex(end),
+ doc = editor.getDoc(),
+ mark = doc.markText(posStart, posEnd, {className: 'editor-mark _emark_' + hintCounter}),
+ result = {start: posStart, end: posEnd, mark: mark, className: '_emark_' + hintCounter};
+ hintCleaners.push(function () { mark.clear(); mark = null; doc = null; result.mark = null; });
+ hintCounter++;
+ return result;
+ },
+ processTemplate = function (template, args) {
+ if (!args)
+ return template;
+ return template.replace(/\[%=(\w+)%\]/g, function(match, name) {
+ return args[name];
+ });
+ },
+ hintHandlers = {
+ 'static': function (type, template, serverHint) {
+ codeq.log.debug('Processing static hint');
+ var message = processTemplate(template, serverHint.args);
+ jqHints.append('<div class="hint-static">' + message + '</div>');
+ // no hint cleaner here, a static hint remains on the screen
+ },
+ 'popup': function (type, template, serverHint) {
+ codeq.log.debug('Processing popup hint');
+ var message = processTemplate(template, serverHint.args),
+ mark = addMark(serverHint.start, serverHint.end), // add the mark
+ jqMark = jqEditor.find('.' + mark.className);
+ /* jqPopup = null,
+ onBlur = function () {
+ codeq.log.debug('Removing popup');
+ if (jqPopup) {
+ jqPopup.off('blur', onBlur);
+ jqPopup.remove();
+ jqPopup = null;
+ }
+ };
+
+ window.jqMark = jqMark; // TODO: DEBUG
+
+ jqMark.on('click', function () {
+ if (jqPopup) return;
+ codeq.log.debug('Showing popup');
+ var pos = mark.mark.find(), // results in {from: {line: number, ch: number}, to: {line: number, ch: number}}
+ left = pos.from.ch < pos.to.ch ? pos.from.ch : pos.to.ch,
+ down = pos.from.line < pos.to.line ? pos.to.line : pos.from.line;
+ jqPopup = $('<div style="position: absolute;" class="editor-popup ' + mark.className + '"></div>').html(template);
+ editor.addWidget({line: down, ch: left}, jqPopup[0], true);
+ jqPopup = jqEditor.find('.editor-popup.' + mark.className);
+ setTimeout(function () {jqPopup.trigger('focus'); jqPopup.on('click', onBlur);}, 50); // event handlers can be registered only when the DOM elements is part of the document
+ window.jqPopup = jqPopup; // TODO: DEBUG
+ });
+
+ hintCleaners.push(function () {
+ if (jqPopup) {
+ jqPopup.off('blur', onBlur);
+ jqPopup.remove();
+ jqPopup = null;
+ }
+ if (jqMark) {
+ jqMark = null;
+ }
+ mark = null;
+ });*/
+
+ jqMark.popover({content: message, html: true, placement: 'auto bottom', trigger: 'hover focus click', container: 'body'});
+ hintCleaners.push(function () { if (jqMark) { jqMark.popover('destroy'); jqMark = null; } });
+
+ mark.mark.on('', function () {});
+ },
+ 'dropdown': function (type, template, serverHint) {
+ codeq.log.debug('Processing dropdown hint');
+ var completion = null, // the completion object, created in showHint()
+ close = function () {
+ if (completion) {
+ completion.close();
+ completion = null;
+ }
+ };
+
+ if ((editor.listSelections().length > 1) || editor.somethingSelected()) {
+ // showHint() doesn't work if a selection is activeparts
+ }
+
+ editor.showHint({
+ hint: function () {
+ var hints = {
+ list: serverHint.choices,
+ from: editor.posFromIndex(serverHint.start),
+ to: editor.posFromIndex(serverHint.end)
+ };
+ completion = editor.state.completionActive;
+ return hints;
+ },
+ completeOnSingleClick: true,
+ completeSingle: false
+ });
+
+ hintCleaners.push(close);
+ }
+ };
+
+ editor.setValue(info.solution);
+ $('#title').text(problem.slug);
+ jqDescription.html(problem.description);
+
+ editor.on('change', function (instance, changeObj) {
+ var doc = editor.getDoc(),
+ pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from));
+
+ if (changeObj.removed) {
+ activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'len': codeq.codePointCount(changeObj.removed)});
+ }
+
+ if (changeObj.text) {
+ activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text});
+ }
+ });
+
+ var handler = {
+ destroy: function () {
+ terminal.destroy();
+ jqDescription.empty();
+ jqEditor.empty(); // TODO: perhaps you do not want to "free" the editor, just empty it
+ jqConsole.empty(); // TODO: the same with the console
+ jqHints.empty(); // TODO: just make static references to these
+ jqDescription = null;
+ jqEditor = null;
+ jqConsole = null;
+ jqHints = null;
+ },
+
+ /**
+ * Processes and display appropriately the server hints.
+ * TODO: sort hints so static and popup hints come first, and a (single) drop-down hint last
+ *
+ * @param {ServerHint[]} serverHints an array of hints from the server
+ */
+ processServerHints: function (serverHints) {
+ var n = serverHints.length,
+ /** number */ i,
+ /** ServerHint */ serverHint,
+ /** HintDefinition */ hintDef,
+ hintType, hintTemplate, t, fn;
+ clearHints();
+ for (i = 0; i < n; i++) {
+ serverHint = serverHints[i];
+ hintDef = hintDefs[serverHint.id];
+ if (serverHint.indices) {
+ indices = serverHint.indices
+ for (i = 0; i < indices.length; i++) {
+ hintDef = hintDef[indices[i]];
+ if (!hintDef)
+ break;
+ }
+ }
+ if (!hintDef) {
+ codeq.log.error('Undefined hint ' + serverHint.id + ' with indices ' + serverHint.indices);
+ continue;
+ }
+ t = typeof hintDef;
+ if (t === 'object') {
+ hintType = hintDef.type;
+ hintTemplate = hintDef.message;
+ }
+ else if (t === 'string') {
+ hintType = 'static';
+ hintTemplate = hintDef;
+ }
+ else {
+ codeq.log.error('Unsupported hint definition: ' + t);
+ continue;
+ }
+
+ fn = hintHandlers[hintType];
+ if (!fn) codeq.log.error('Unsupported hint type: ' + hintType);
+ else fn(hintType, hintTemplate, serverHint);
+ }
+ }
+ };
+
+ var jqButtons = $('#block-toolbar button');
+// $(jqButtons.get(0)).on('click', function () { handler.processServerHints([{id:'x_must_be_female'}]); });
+// $(jqButtons.get(1)).on('click', function () { handler.processServerHints([{id:'popup_unknown', start: 20, end: 26}]); });
+// $(jqButtons.get(2)).on('click', function () { handler.processServerHints([{id:'drop_down', start: 20, end: 26, choices:['ena', 'dva', 'tri']}]); });
+
+ $('#btn_code_hint').on('click', function () {
+ if (language === 'prolog') {
+ terminal.append('hint.\n', 'input');
+ terminal.inputDisable();
+ }
+ var doc = editor.getDoc();
+ codeq.comms.sendHint({
+ 'language': 'prolog',
+ 'program': editor.getDoc().getValue(),
+ 'problem_id': problem.id
+ }).then(
+ function hintSuccess(data) {
+ if (data.code === 0)
+ handler.processServerHints(data.hints);
+ else
+ terminal.append(data.message + '\n', 'error');
+ },
+ function hintFailed (error) {
+ terminal.append(error + '\n', 'error');
+ }
+ ).fin(function () {
+ if(language === 'prolog'){
+ terminal.inputEnable();
+ terminal.append('?- ', 'output');
+ }
+ }
+ ).done();
+ });
+ $('#btn_code_test').on('click', function () {
+ if(language === 'prolog'){
+ terminal.append('test.\n', 'input');
+ terminal.inputDisable();
+ }
+ var doc = editor.getDoc();
+ codeq.comms.sendTest({
+ 'language': language,
+ 'program': editor.getDoc().getValue(),
+ 'problem_id': problem.id
+ }).then(
+ function testSuccess(data) {
+ if (data.code === 0)
+ handler.processServerHints(data.hints);
+ else
+ terminal.append(data.message + '\n', 'error');
+ },
+ function testFailed (error) {
+ terminal.append(error + '\n', 'error');
+ }
+ ).fin(function () {
+ if(language === 'prolog'){
+ terminal.inputEnable();
+ terminal.append('?- ', 'output');
+ }
+ }
+ ).done();
+ });
+
+ if(language === 'python'){
+ // TODO first line of interpreter output is buffered without this, why?
+ codeq.comms.sendPush({
+ 'text': ''
+ });
+ }
+
+ return handler;
+};
diff --git a/js/codeq/python.js b/js/codeq/python.js
new file mode 100644
index 0000000..3445db5
--- /dev/null
+++ b/js/codeq/python.js
@@ -0,0 +1,339 @@
+/**
+ * Created by robert on 9/17/15.
+ */
+
+(function() {
+ var problems,//this will the actual (sub)state machine
+ stateNameTag = 'stateName';
+
+ //get the divs which are the main elements being change on transitions between states in the sub-state machine
+ var divs = {};
+ divs['description'] = $('#description_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#description)');
+ divs['code'] = $('#code_editor_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#code_editor)');
+ divs['console'] = $('#console_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#console)');
+ divs['info'] = $('#info_outer_div');//$('div.col-lg-3.col-md-6.col-sm-12:has(#info)');
+
+ //these tags connect the div to their respective state in the sub-state machine
+ divs['description'].data(stateNameTag, 'description');
+ divs['code'].data(stateNameTag, 'code');
+ divs['console'].data(stateNameTag, 'console');
+ divs['info'].data(stateNameTag, 'info');
+
+ var eventName = 'mousedown',//event name of the event which will trigger the transition between these substates
+ /**
+ * the general function which should be called on the mousedown event to trigger the transition between states
+ */
+ mouseDownEventFunction = function () {
+ problems.transition($(this).data(stateNameTag));
+ },
+ /**
+ * removes the above function from the divs
+ */
+ removeListenersFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.off(eventName, mouseDownEventFunction);
+ });
+ },
+ /**
+ * removes the 'block' class from all divs we need to change when we change the current state
+ */
+ removeBlockClassesFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.removeClass('block');
+ });
+ },
+ /**
+ * removes all those special classes from the divs, which are set when a new state is entered and it also adds the 'block' class back
+ */
+ removeChangedClassesFromDivs = function () {
+ $.each(divs, function (i, value) {
+ value.removeClass('block-focus block-less-width block-less-height block-less-everything').addClass('block');//these class names were chosen for the view where the screen is partitioned in 4 quarters (2 by 2)
+ });
+ },
+ /**
+ *
+ * @param current the key of the state ('description','code','hint','console'), which is currently active, because it must not have a transition set
+ */
+ setTransitionsBetweenDivs = function (current) {
+ $.each(divs, function (key, value) {
+ if (current !== key) value.on(eventName, mouseDownEventFunction);
+ });
+ },
+ /**
+ *
+ * @param divsWithClasses a object where we have the keys of the divs object paired with the class name we want to assign to them e.g. :
+ * {
+ 'description': 'block-focus',
+ 'code': 'block-less-width',
+ 'console': 'block-less-height',
+ 'info': 'block-less-everything'
+ }
+ */
+ addClassesToDivs = function (divsWithClasses) {
+ $.each(divsWithClasses, function (divName, className) {
+ divs[divName].addClass(className);
+ });
+ },
+ substates = {
+ 'description': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-focus',
+ 'code': 'block-less-width',
+ 'console': 'block-less-height',
+ 'info': 'block-less-everything'
+ });
+ setTransitionsBetweenDivs('description');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ },
+ 'code': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-width',
+ 'code': 'block-focus',
+ 'console': 'block-less-everything',
+ 'info': 'block-less-height'
+ });
+ setTransitionsBetweenDivs('code');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ },
+ 'info': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-everything',
+ 'code': 'block-less-height',
+ 'console': 'block-less-width',
+ 'info': 'block-focus'
+ });
+ setTransitionsBetweenDivs('info');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ },
+ 'console': {
+ 'enter': function () {
+ removeBlockClassesFromDivs();
+ addClassesToDivs({
+ 'description': 'block-less-height',
+ 'code': 'block-less-everything',
+ 'console': 'block-focus',
+ 'info': 'block-less-width'
+ });
+ setTransitionsBetweenDivs('console');
+ },
+ 'exit': function () {
+ removeChangedClassesFromDivs();
+ removeListenersFromDivs();
+ }
+ }
+ };
+ var pythonHandler;
+ codeq.globalStateMachine.register('python', {
+ 'enter': function (data) {
+ $('#disabled').css('display', '');
+ $('#screen_prolog').css('display', '');//we have to show the screen now so the code editor shopws its initial values correctly
+ pythonHandler = createPythonHandler(data.data);//codeq.createProgrammingLanguageHandler('prolog',data.data,codeq.makePythonTerminalHandler);
+ problems = codeq.makeStateMachine(substates);
+ problems.transition(divs['description'].data(stateNameTag));
+ Q.delay(100).then(function(){
+ $('div.col-lg-3.col-md-6.col-sm-12').addClass('transition');
+ }).done();
+ $('#disabled').css('display', 'none');
+ },
+ 'exit': function () {
+ $('#screen_prolog').css('display', 'none');
+ $('div.col-lg-3.col-md-6.col-sm-12').removeClass('transition');
+ pythonHandler.destroy();
+ problems.destroy();
+ }
+ });
+
+ // a constant
+ var firstCharacterPos = {'line': 0, 'ch': 0};
+
+ //codeq.makePythonTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) {
+ var makePythonTerminalHandler = function (jqConsole, editor, problem_id, activityHandler) {
+ var terminal = codeq.makeConsole(jqConsole, {
+ 'greeting': 'CodeQ Python terminal proxy'
+ }),
+ tcs = function terminalCommandSuccess (data) {
+ if (data.code !== 0) {
+ terminal.append(data.message, 'error');
+ }
+ },
+ tcf = function terminalCommandFailed (error) {
+ terminal.append(error + '\n', 'error');
+ };
+
+ terminal.onInput = function (text) {
+ return codeq.comms.sendPush({
+ 'text': text + '\n'
+ }).then(tcs, tcf);
+ };
+
+ codeq.comms.on('terminal_output', function (data) {
+ var text = data.text;
+ terminal.append(text, 'output');
+ lines = text.split('\n');
+ terminal.leftmostCol = lines[lines.length-1].length;
+ });
+
+ terminal.leftmostCol = 1;
+
+ return terminal;
+ };
+
+ var makeActivityHandler = function (editor, problem_id) {
+ var lastActivityMillis = Date.now(),
+ deltaActivityMillis = function deltaActivityMillisFunc () {
+ var now = Date.now(),
+ dt = now - lastActivityMillis;
+ lastActivityMillis = now;
+ return dt;
+ },
+ queue = [],
+ ts = null,
+ timer = function () {
+ var promise;
+ ts = null;
+ if (queue.length === 0) return Q(true);
+ promise = codeq.comms.sendActivity(queue, editor.getDoc().getValue(), problem_id);
+ queue.length = 0;
+ return promise;
+ },
+ flush = function () {
+ clearTimeout(ts);
+ return timer();
+ };
+
+ return {
+ 'queueTrace': function (trace) {
+ trace['dt'] = deltaActivityMillis();
+ queue.push(trace);
+ if (ts === null) ts = setTimeout(timer, 10000); // flush every 10 seconds
+ return this;
+ },
+ 'flush': flush,
+ 'addAndPurge': function (trace) {
+ var accumulatedTrace = queue;
+ queue = [];
+ trace['dt'] = deltaActivityMillis();
+ accumulatedTrace.push(trace);
+ if (ts !== null) {
+ clearTimeout(ts);
+ ts = null;
+ }
+ return accumulatedTrace;
+ }
+ };
+ };
+
+
+
+ /**
+ * Creates a new handler for the given Prolog assignment definition.
+ *
+ * @param {PrologTaskDef} info
+ * @returns {{destroy: Function, processServerHints: Function}}
+ */
+ var createPythonHandler = function (info) {
+ var problem = info.problem,
+ jqDescription = $('#description'),
+ jqEditor = $('#code_editor'),
+ jqConsole = $('#console'),
+ jqHints = $('#info'),
+ editor = CodeMirror(jqEditor[0], { cursorHeight: 0.85, lineNumbers: true, matchBrackets: true, mode: 'python' }),
+ activityHandler = makeActivityHandler(editor, problem.id),
+ terminal = makePythonTerminalHandler(jqConsole, editor, problem.id, activityHandler),
+ hinter = codeq.makeHinter(jqHints, jqEditor, editor, problem.hint);
+
+ editor.setValue(info.solution);
+ $('#title').text(problem.slug);
+ jqDescription.html(problem.description);
+
+ editor.on('change', function (instance, changeObj) {
+ var doc = editor.getDoc(),
+ pos = codeq.codePointCount(doc.getRange(firstCharacterPos, changeObj.from));
+
+ if (changeObj.removed) {
+ activityHandler.queueTrace({'typ': 'rm', 'off': pos, 'len': codeq.codePointCount(changeObj.removed)});
+ }
+
+ if (changeObj.text) {
+ activityHandler.queueTrace({'typ': 'ins', 'off': pos, 'txt': changeObj.text});
+ }
+ });
+
+ $('#btn_code_hint').on('click', function () {
+ var doc = editor.getDoc();
+ codeq.comms.sendHint({
+ 'language': 'python',
+ 'program': editor.getDoc().getValue(),
+ 'problem_id': problem.id
+ }).then(
+ function hintSuccess(data) {
+ if (data.code === 0)
+ hinter.handle(data.hints);
+ else
+ terminal.append('error: ' + data.message);
+ },
+ function hintFailed (error) {
+ terminal.append('exception: ' + error);
+ }
+ ).done();
+ });
+ $('#btn_code_test').on('click', function () {
+ var doc = editor.getDoc();
+ codeq.comms.sendTest({
+ 'language': 'python',
+ 'program': editor.getDoc().getValue(),
+ 'problem_id': problem.id
+ }).then(
+ function testSuccess(data) {
+ if (data.code === 0)
+ hinter.handle(data.hints);
+ else
+ terminal.append('error: ' + data.message);
+ },
+ function testFailed (error) {
+ terminal.append('exception: ' + error);
+ }
+ ).done();
+ });
+
+ // TODO first line of interpreter output is buffered without this, why?
+ codeq.comms.sendPush({
+ 'text': ''
+ });
+
+ return {
+ destroy: function () {
+ $('#btn_code_hint').off('click');
+ $('#btn_code_test').off('click');
+ editor.off('change');
+ hinter.destroy();
+ terminal.destroy();
+ jqDescription.empty();
+ jqEditor.empty(); // TODO: perhaps you do not want to "free" the editor, just empty it
+ jqConsole.empty(); // TODO: the same with the console
+ jqDescription = null;
+ jqEditor = null;
+ jqConsole = null;
+ jqHints = null;
+ }
+ };
+ };
+})();
diff --git a/js/codeq/startup.js b/js/codeq/startup.js
index 730a9f8..ccef9f9 100644
--- a/js/codeq/startup.js
+++ b/js/codeq/startup.js
@@ -1,5 +1,5 @@
$(document).ready(function () {
- codeq.comms.connect().then(function () {
+ /*codeq.comms.connect().then(function () {
return codeq.comms.send({'action': 'list_problems'});
}).then(
function success(data) {
@@ -94,5 +94,7 @@ $(document).ready(function () {
$('#disabled').css('cursor', '');
alert('Request to obtain list of problems failed: ' + reason);
}
- ).done();
+ ).done();*/
+
+ codeq.globalStateMachine.transition('login');
}); \ No newline at end of file
diff --git a/js/codeq/stateMachine.js b/js/codeq/stateMachine.js
new file mode 100644
index 0000000..cfb27e7
--- /dev/null
+++ b/js/codeq/stateMachine.js
@@ -0,0 +1,22 @@
+/**
+ * Created by robert on 9/15/15.
+ */
+
+codeq.makeStateMachine = function(def){
+ var currState = null;
+ return {
+ 'transition': function(name){
+ if(currState !== null) currState.exit();
+ currState = def[name];
+ currState.enter.apply(currState,Array.prototype.slice.apply(arguments,[1]));
+ },
+ 'destroy': function(){
+ if(currState !== null) currState.exit();
+ currState = null;
+ },
+ 'register': function(name,state){
+ def[name] = state;
+ }
+ }
+};
+codeq.globalStateMachine = codeq.makeStateMachine({}); \ No newline at end of file