diff options
Diffstat (limited to 'js/codeq/prolog.js')
-rw-r--r-- | js/codeq/prolog.js | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/js/codeq/prolog.js b/js/codeq/prolog.js new file mode 100644 index 0000000..1978ba4 --- /dev/null +++ b/js/codeq/prolog.js @@ -0,0 +1,450 @@ +/** + * Created by robert on 9/17/15. + * + * The prolog state of the state machine. When it is entered it'll prepare the console and code editor and load a sub-state machine which represents the 4 different parts fo the screen. + */ + +(function() { + var subScreens,//this will the actual (sub)state machine + stateNameTag = 'stateName';//a tag for data which is added to some html elements + + //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'); + divs['code'] = $('#code_editor_outer_div'); + divs['console'] = $('#console_outer_div'); + divs['info'] = $('#info_outer_div'); + + //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 () { + subScreens.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(){ + subScreens.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); + subScreens = codeq.makeStateMachine(substates); + subScreens.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(); + subScreens.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 |